refactor: Update symbol type to use lowercase 'symbol' in ColumnAccessControl files

This commit is contained in:
Simon Larsen
2024-06-14 12:09:53 +01:00
parent 5152d5de12
commit 70a2a3993b
1729 changed files with 217499 additions and 225678 deletions

View File

@@ -1,11 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderParserPlugins": ["typescript", "decorators", "dynamicImport", "jsx"]
}

View File

@@ -1,37 +1,37 @@
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes'; import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import Express, { ExpressApplication } from 'CommonServer/Utils/Express'; import Express, { ExpressApplication } from "CommonServer/Utils/Express";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import App from 'CommonServer/Utils/StartServer'; import App from "CommonServer/Utils/StartServer";
export const APP_NAME: string = 'accounts'; export const APP_NAME: string = "accounts";
const app: ExpressApplication = Express.getExpressApp(); const app: ExpressApplication = Express.getExpressApp();
const init: PromiseVoidFunction = async (): Promise<void> => { const init: PromiseVoidFunction = async (): Promise<void> => {
try { try {
// init the app // init the app
await App.init({ await App.init({
appName: APP_NAME, appName: APP_NAME,
port: undefined, port: undefined,
isFrontendApp: true, isFrontendApp: true,
statusOptions: { statusOptions: {
liveCheck: async () => {}, liveCheck: async () => {},
readyCheck: async () => {}, readyCheck: async () => {},
}, },
}); });
// add default routes // add default routes
await App.addDefaultRoutes(); await App.addDefaultRoutes();
} catch (err) { } catch (err) {
logger.error('App Init Failed:'); logger.error("App Init Failed:");
logger.error(err); logger.error(err);
throw err; throw err;
} }
}; };
init().catch((err: Error) => { init().catch((err: Error) => {
logger.error(err); logger.error(err);
logger.error('Exiting node process'); logger.error("Exiting node process");
process.exit(1); process.exit(1);
}); });
export default app; export default app;

8
Accounts/index.d.ts vendored
View File

@@ -1,4 +1,4 @@
declare module '*.png'; declare module "*.png";
declare module '*.svg'; declare module "*.svg";
declare module '*.jpg'; declare module "*.jpg";
declare module '*.gif'; declare module "*.gif";

View File

@@ -1,47 +1,44 @@
import ForgotPasswordPage from './Pages/ForgotPassword'; import ForgotPasswordPage from "./Pages/ForgotPassword";
import LoginPage from './Pages/Login'; import LoginPage from "./Pages/Login";
import NotFound from './Pages/NotFound'; import NotFound from "./Pages/NotFound";
import RegisterPage from './Pages/Register'; import RegisterPage from "./Pages/Register";
import ResetPasswordPage from './Pages/ResetPassword'; import ResetPasswordPage from "./Pages/ResetPassword";
import VerifyEmail from './Pages/VerifyEmail'; import VerifyEmail from "./Pages/VerifyEmail";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import React, { ReactElement } from 'react'; import React, { ReactElement } from "react";
import { import {
Route, Route,
Routes, Routes,
useLocation, useLocation,
useNavigate, useNavigate,
useParams, useParams,
} from 'react-router-dom'; } from "react-router-dom";
function App(): ReactElement { function App(): ReactElement {
Navigation.setNavigateHook(useNavigate()); Navigation.setNavigateHook(useNavigate());
Navigation.setLocation(useLocation()); Navigation.setLocation(useLocation());
Navigation.setParams(useParams()); Navigation.setParams(useParams());
return ( return (
<div className="m-auto h-screen"> <div className="m-auto h-screen">
<Routes> <Routes>
<Route path="/accounts" element={<LoginPage />} /> <Route path="/accounts" element={<LoginPage />} />
<Route path="/accounts/login" element={<LoginPage />} /> <Route path="/accounts/login" element={<LoginPage />} />
<Route <Route
path="/accounts/forgot-password" path="/accounts/forgot-password"
element={<ForgotPasswordPage />} element={<ForgotPasswordPage />}
/> />
<Route <Route
path="/accounts/reset-password/:token" path="/accounts/reset-password/:token"
element={<ResetPasswordPage />} element={<ResetPasswordPage />}
/> />
<Route path="/accounts/register" element={<RegisterPage />} /> <Route path="/accounts/register" element={<RegisterPage />} />
<Route <Route path="/accounts/verify-email/:token" element={<VerifyEmail />} />
path="/accounts/verify-email/:token" {/* 👇️ only match this when no other routes match */}
element={<VerifyEmail />} <Route path="*" element={<NotFound />} />
/> </Routes>
{/* 👇️ only match this when no other routes match */} </div>
<Route path="*" element={<NotFound />} /> );
</Routes>
</div>
);
} }
export default App; export default App;

View File

@@ -1,20 +1,20 @@
import React from 'react'; import React from "react";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
const Footer: () => JSX.Element = () => { const Footer: () => JSX.Element = () => {
return ( return (
<div className="footer"> <div className="footer">
<p> <p>
<Link to="/">&copy; OneUptime</Link> <Link to="/">&copy; OneUptime</Link>
</p> </p>
<p> <p>
<Link to="/">Contact</Link> <Link to="/">Contact</Link>
</p> </p>
<p> <p>
<Link to="/">Privacy &amp; terms</Link> <Link to="/">Privacy &amp; terms</Link>
</p> </p>
</div> </div>
); );
}; };
export default Footer; export default Footer;

View File

@@ -1,19 +1,19 @@
import App from './App'; import App from "./App";
import Telemetry from 'CommonUI/src/Utils/Telemetry'; import Telemetry from "CommonUI/src/Utils/Telemetry";
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom/client'; import ReactDOM from "react-dom/client";
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from "react-router-dom";
Telemetry.init({ Telemetry.init({
serviceName: 'Accounts', serviceName: "Accounts",
}); });
const root: any = ReactDOM.createRoot( const root: any = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById("root") as HTMLElement,
); );
root.render( root.render(
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>,
); );

View File

@@ -1,99 +1,99 @@
import { FORGOT_PASSWORD_API_URL } from '../Utils/ApiPaths'; import { FORGOT_PASSWORD_API_URL } from "../Utils/ApiPaths";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import ModelForm, { FormType } from 'CommonUI/src/Components/Forms/ModelForm'; import ModelForm, { FormType } from "CommonUI/src/Components/Forms/ModelForm";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import Link from 'CommonUI/src/Components/Link/Link'; import Link from "CommonUI/src/Components/Link/Link";
import OneUptimeLogo from 'CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg'; import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
import User from 'Model/Models/User'; import User from "Model/Models/User";
import React, { useState } from 'react'; import React, { useState } from "react";
const ForgotPassword: () => JSX.Element = () => { const ForgotPassword: () => JSX.Element = () => {
const apiUrl: URL = FORGOT_PASSWORD_API_URL; const apiUrl: URL = FORGOT_PASSWORD_API_URL;
const [isSuccess, setIsSuccess] = useState<boolean>(false); const [isSuccess, setIsSuccess] = useState<boolean>(false);
return ( return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8"> <div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md"> <div className="sm:mx-auto sm:w-full sm:max-w-md">
<img <img
className="mx-auto h-12 w-auto" className="mx-auto h-12 w-auto"
src={OneUptimeLogo} src={OneUptimeLogo}
alt="Your Company" alt="Your Company"
/> />
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900"> <h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
Forgot your password Forgot your password
</h2> </h2>
{!isSuccess && ( {!isSuccess && (
<p className="mt-2 text-center text-sm text-gray-600"> <p className="mt-2 text-center text-sm text-gray-600">
Please enter your email and the password reset link will Please enter your email and the password reset link will be sent to
be sent to you. you.
</p> </p>
)} )}
{isSuccess && ( {isSuccess && (
<p className="mt-2 text-center text-sm text-gray-600"> <p className="mt-2 text-center text-sm text-gray-600">
We have emailed you the password reset link. Please do We have emailed you the password reset link. Please do not forget to
not forget to check spam. check spam.
</p> </p>
)} )}
</div> </div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
{!isSuccess && ( {!isSuccess && (
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<ModelForm<User> <ModelForm<User>
modelType={User} modelType={User}
name="Forgot Password" name="Forgot Password"
id="login-form" id="login-form"
createOrUpdateApiUrl={apiUrl} createOrUpdateApiUrl={apiUrl}
fields={[ fields={[
{ {
field: { field: {
email: true, email: true,
}, },
title: 'Email', title: "Email",
fieldType: FormFieldSchemaType.Email, fieldType: FormFieldSchemaType.Email,
required: true, required: true,
}, },
]} ]}
onSuccess={() => { onSuccess={() => {
setIsSuccess(true); setIsSuccess(true);
}} }}
submitButtonText={'Send Password Reset Link'} submitButtonText={"Send Password Reset Link"}
formType={FormType.Create} formType={FormType.Create}
maxPrimaryButtonWidth={true} maxPrimaryButtonWidth={true}
footer={ footer={
<div className="actions pointer text-center mt-4 hover:underline fw-semibold"> <div className="actions pointer text-center mt-4 hover:underline fw-semibold">
<p> <p>
<Link <Link
to={new Route('/accounts/login')} to={new Route("/accounts/login")}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm" className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm"
> >
Return to Sign in. Return to Sign in.
</Link> </Link>
</p> </p>
</div>
}
/>
</div>
)}
<div className="mt-5 text-center">
<p className="text-muted mb-0 text-gray-500">
Remember your password?{' '}
<Link
to={new Route('/accounts/login')}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
>
Login.
</Link>
</p>
</div> </div>
</div> }
/>
</div>
)}
<div className="mt-5 text-center">
<p className="text-muted mb-0 text-gray-500">
Remember your password?{" "}
<Link
to={new Route("/accounts/login")}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
>
Login.
</Link>
</p>
</div> </div>
); </div>
</div>
);
}; };
export default ForgotPassword; export default ForgotPassword;

View File

@@ -1,167 +1,161 @@
import { LOGIN_API_URL } from '../Utils/ApiPaths'; import { LOGIN_API_URL } from "../Utils/ApiPaths";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import Alert, { AlertType } from 'CommonUI/src/Components/Alerts/Alert'; import Alert, { AlertType } from "CommonUI/src/Components/Alerts/Alert";
import ModelForm, { FormType } from 'CommonUI/src/Components/Forms/ModelForm'; import ModelForm, { FormType } from "CommonUI/src/Components/Forms/ModelForm";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import Link from 'CommonUI/src/Components/Link/Link'; import Link from "CommonUI/src/Components/Link/Link";
import { DASHBOARD_URL } from 'CommonUI/src/Config'; import { DASHBOARD_URL } from "CommonUI/src/Config";
import OneUptimeLogo from 'CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg'; import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
import UiAnalytics from 'CommonUI/src/Utils/Analytics'; import UiAnalytics from "CommonUI/src/Utils/Analytics";
import LoginUtil from 'CommonUI/src/Utils/Login'; import LoginUtil from "CommonUI/src/Utils/Login";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import UserUtil from 'CommonUI/src/Utils/User'; import UserUtil from "CommonUI/src/Utils/User";
import User from 'Model/Models/User'; import User from "Model/Models/User";
import React, { useState } from 'react'; import React, { useState } from "react";
import useAsyncEffect from 'use-async-effect'; import useAsyncEffect from "use-async-effect";
const LoginPage: () => JSX.Element = () => { const LoginPage: () => JSX.Element = () => {
const apiUrl: URL = LOGIN_API_URL; const apiUrl: URL = LOGIN_API_URL;
if (UserUtil.isLoggedIn()) { if (UserUtil.isLoggedIn()) {
Navigation.navigate(DASHBOARD_URL); Navigation.navigate(DASHBOARD_URL);
}
const showSsoMessage: boolean = Boolean(
Navigation.getQueryStringByName("sso"),
);
const [showSsoTip, setShowSSOTip] = useState<boolean>(false);
const [initialValues, setInitialValues] = React.useState<JSONObject>({});
useAsyncEffect(async () => {
if (Navigation.getQueryStringByName("email")) {
setInitialValues({
email: Navigation.getQueryStringByName("email"),
});
} }
}, []);
const showSsoMessage: boolean = Boolean( return (
Navigation.getQueryStringByName('sso') <div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
); <div className="">
<img
className="mx-auto h-12 w-auto"
src={OneUptimeLogo}
alt="OneUptime"
/>
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Join thousands of business that use OneUptime to help them stay online
all the time.
</p>
</div>
const [showSsoTip, setShowSSOTip] = useState<boolean>(false); {showSsoMessage && (
<div className="sm:mx-auto sm:w-full sm:max-w-md mt-8">
const [initialValues, setInitialValues] = React.useState<JSONObject>({}); {" "}
<Alert
useAsyncEffect(async () => { type={AlertType.DANGER}
if (Navigation.getQueryStringByName('email')) { title="You must be logged into OneUptime account to use single sign-on (SSO) for your project. Logging in to OneUptime account and single sign on (SSO) for your project are two separate steps. Please use the form below to log in to your OneUptime account before you use SSO."
setInitialValues({ />{" "}
email: Navigation.getQueryStringByName('email'),
});
}
}, []);
return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="">
<img
className="mx-auto h-12 w-auto"
src={OneUptimeLogo}
alt="OneUptime"
/>
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Join thousands of business that use OneUptime to help them
stay online all the time.
</p>
</div>
{showSsoMessage && (
<div className="sm:mx-auto sm:w-full sm:max-w-md mt-8">
{' '}
<Alert
type={AlertType.DANGER}
title="You must be logged into OneUptime account to use single sign-on (SSO) for your project. Logging in to OneUptime account and single sign on (SSO) for your project are two separate steps. Please use the form below to log in to your OneUptime account before you use SSO."
/>{' '}
</div>
)}
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<ModelForm<User>
modelType={User}
id="login-form"
name="Login"
fields={[
{
field: {
email: true,
},
fieldType: FormFieldSchemaType.Email,
placeholder: 'jeff@example.com',
required: true,
disabled: Boolean(
initialValues && initialValues['email']
),
title: 'Email',
dataTestId: 'email',
},
{
field: {
password: true,
},
title: 'Password',
required: true,
validation: {
minLength: 6,
},
fieldType: FormFieldSchemaType.Password,
sideLink: {
text: 'Forgot password?',
url: new Route('/accounts/forgot-password'),
openLinkInNewTab: false,
},
dataTestId: 'password',
},
]}
createOrUpdateApiUrl={apiUrl}
formType={FormType.Create}
submitButtonText={'Login'}
onSuccess={(
value: User,
miscData: JSONObject | undefined
) => {
if (value && value.email) {
UiAnalytics.userAuth(value.email);
UiAnalytics.capture('accounts/login');
}
LoginUtil.login({
user: value,
token: miscData ? miscData['token'] : undefined,
});
}}
maxPrimaryButtonWidth={true}
footer={
<div className="actions text-center mt-4 hover:underline fw-semibold">
<div>
{!showSsoTip && (
<div
onClick={() => {
setShowSSOTip(true);
}}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm"
>
Use single sign-on (SSO) instead
</div>
)}
{showSsoTip && (
<div className="text-gray-500 text-sm">
Please sign in with your SSO
provider like Okta, Auth0, Entra ID
or any other SAML 2.0 provider.
</div>
)}
</div>
</div>
}
/>
</div>
<div className="mt-10 text-center">
<div className="text-muted mb-0 text-gray-500">
Don&apos;t have an account?{' '}
<Link
to={new Route('/accounts/register')}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
>
Register.
</Link>
</div>
</div>
</div>
</div> </div>
); )}
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<ModelForm<User>
modelType={User}
id="login-form"
name="Login"
fields={[
{
field: {
email: true,
},
fieldType: FormFieldSchemaType.Email,
placeholder: "jeff@example.com",
required: true,
disabled: Boolean(initialValues && initialValues["email"]),
title: "Email",
dataTestId: "email",
},
{
field: {
password: true,
},
title: "Password",
required: true,
validation: {
minLength: 6,
},
fieldType: FormFieldSchemaType.Password,
sideLink: {
text: "Forgot password?",
url: new Route("/accounts/forgot-password"),
openLinkInNewTab: false,
},
dataTestId: "password",
},
]}
createOrUpdateApiUrl={apiUrl}
formType={FormType.Create}
submitButtonText={"Login"}
onSuccess={(value: User, miscData: JSONObject | undefined) => {
if (value && value.email) {
UiAnalytics.userAuth(value.email);
UiAnalytics.capture("accounts/login");
}
LoginUtil.login({
user: value,
token: miscData ? miscData["token"] : undefined,
});
}}
maxPrimaryButtonWidth={true}
footer={
<div className="actions text-center mt-4 hover:underline fw-semibold">
<div>
{!showSsoTip && (
<div
onClick={() => {
setShowSSOTip(true);
}}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm"
>
Use single sign-on (SSO) instead
</div>
)}
{showSsoTip && (
<div className="text-gray-500 text-sm">
Please sign in with your SSO provider like Okta, Auth0,
Entra ID or any other SAML 2.0 provider.
</div>
)}
</div>
</div>
}
/>
</div>
<div className="mt-10 text-center">
<div className="text-muted mb-0 text-gray-500">
Don&apos;t have an account?{" "}
<Link
to={new Route("/accounts/register")}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
>
Register.
</Link>
</div>
</div>
</div>
</div>
);
}; };
export default LoginPage; export default LoginPage;

View File

@@ -1,18 +1,18 @@
import React from 'react'; import React from "react";
const LoginPage: () => JSX.Element = () => { const LoginPage: () => JSX.Element = () => {
return ( return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8"> <div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md"> <div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900"> <h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
Page not found Page not found
</h2> </h2>
<p className="mt-2 text-center text-sm text-gray-600"> <p className="mt-2 text-center text-sm text-gray-600">
Page you are looking for does not exist. Page you are looking for does not exist.
</p> </p>
</div> </div>
</div> </div>
); );
}; };
export default LoginPage; export default LoginPage;

View File

@@ -1,276 +1,267 @@
import { SIGNUP_API_URL } from '../Utils/ApiPaths'; import { SIGNUP_API_URL } from "../Utils/ApiPaths";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import Dictionary from 'Common/Types/Dictionary'; import Dictionary from "Common/Types/Dictionary";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage'; import ErrorMessage from "CommonUI/src/Components/ErrorMessage/ErrorMessage";
import ModelForm, { FormType } from 'CommonUI/src/Components/Forms/ModelForm'; import ModelForm, { FormType } from "CommonUI/src/Components/Forms/ModelForm";
import Fields from 'CommonUI/src/Components/Forms/Types/Fields'; import Fields from "CommonUI/src/Components/Forms/Types/Fields";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import Link from 'CommonUI/src/Components/Link/Link'; import Link from "CommonUI/src/Components/Link/Link";
import PageLoader from 'CommonUI/src/Components/Loader/PageLoader'; import PageLoader from "CommonUI/src/Components/Loader/PageLoader";
import { BILLING_ENABLED, DASHBOARD_URL } from 'CommonUI/src/Config'; import { BILLING_ENABLED, DASHBOARD_URL } from "CommonUI/src/Config";
import OneUptimeLogo from 'CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg'; import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
import BaseAPI from 'CommonUI/src/Utils/API/API'; import BaseAPI from "CommonUI/src/Utils/API/API";
import UiAnalytics from 'CommonUI/src/Utils/Analytics'; import UiAnalytics from "CommonUI/src/Utils/Analytics";
import LocalStorage from 'CommonUI/src/Utils/LocalStorage'; import LocalStorage from "CommonUI/src/Utils/LocalStorage";
import LoginUtil from 'CommonUI/src/Utils/Login'; import LoginUtil from "CommonUI/src/Utils/Login";
import ModelAPI, { ListResult } from 'CommonUI/src/Utils/ModelAPI/ModelAPI'; import ModelAPI, { ListResult } from "CommonUI/src/Utils/ModelAPI/ModelAPI";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import UserUtil from 'CommonUI/src/Utils/User'; import UserUtil from "CommonUI/src/Utils/User";
import Reseller from 'Model/Models/Reseller'; import Reseller from "Model/Models/Reseller";
import User from 'Model/Models/User'; import User from "Model/Models/User";
import React, { useState } from 'react'; import React, { useState } from "react";
import useAsyncEffect from 'use-async-effect'; import useAsyncEffect from "use-async-effect";
const RegisterPage: () => JSX.Element = () => { const RegisterPage: () => JSX.Element = () => {
const apiUrl: URL = SIGNUP_API_URL; const apiUrl: URL = SIGNUP_API_URL;
const [initialValues, setInitialValues] = React.useState<JSONObject>({}); const [initialValues, setInitialValues] = React.useState<JSONObject>({});
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>("");
const [isLoading, setIsLoading] = React.useState<boolean>(false); const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [reseller, setResller] = React.useState<Reseller | undefined>( const [reseller, setResller] = React.useState<Reseller | undefined>(
undefined undefined,
); );
if (UserUtil.isLoggedIn()) { if (UserUtil.isLoggedIn()) {
Navigation.navigate(DASHBOARD_URL); Navigation.navigate(DASHBOARD_URL);
}
type FetchResellerFunction = (resellerId: string) => Promise<void>;
const fetchReseller: FetchResellerFunction = async (
resellerId: string,
): Promise<void> => {
setIsLoading(true);
try {
const reseller: ListResult<Reseller> = await ModelAPI.getList<Reseller>({
modelType: Reseller,
query: {
resellerId: resellerId,
},
limit: 1,
skip: 0,
select: {
hidePhoneNumberOnSignup: true,
},
sort: {},
requestOptions: {},
});
if (reseller.data.length > 0) {
setResller(reseller.data[0]);
}
} catch (err) {
setError(BaseAPI.getFriendlyMessage(err));
} }
type FetchResellerFunction = (resellerId: string) => Promise<void>; setIsLoading(false);
};
const fetchReseller: FetchResellerFunction = async ( useAsyncEffect(async () => {
resellerId: string // if promo code is found, please save it in localstorage.
): Promise<void> => { if (Navigation.getQueryStringByName("promoCode")) {
setIsLoading(true); LocalStorage.setItem(
"promoCode",
try { Navigation.getQueryStringByName("promoCode"),
const reseller: ListResult<Reseller> = );
await ModelAPI.getList<Reseller>({
modelType: Reseller,
query: {
resellerId: resellerId,
},
limit: 1,
skip: 0,
select: {
hidePhoneNumberOnSignup: true,
},
sort: {},
requestOptions: {},
});
if (reseller.data.length > 0) {
setResller(reseller.data[0]);
}
} catch (err) {
setError(BaseAPI.getFriendlyMessage(err));
}
setIsLoading(false);
};
useAsyncEffect(async () => {
// if promo code is found, please save it in localstorage.
if (Navigation.getQueryStringByName('promoCode')) {
LocalStorage.setItem(
'promoCode',
Navigation.getQueryStringByName('promoCode')
);
}
if (Navigation.getQueryStringByName('email')) {
setInitialValues({
email: Navigation.getQueryStringByName('email'),
});
}
// if promo code is found, please save it in localstorage.
if (Navigation.getQueryStringByName('partnerId')) {
await fetchReseller(Navigation.getQueryStringByName('partnerId')!);
}
}, []);
let formFields: Fields<User> = [
{
field: {
email: true,
},
fieldType: FormFieldSchemaType.Email,
placeholder: 'jeff@example.com',
required: true,
disabled: Boolean(initialValues && initialValues['email']),
title: 'Email',
dataTestId: 'email',
},
{
field: {
name: true,
},
fieldType: FormFieldSchemaType.Text,
placeholder: 'Jeff Smith',
required: true,
title: 'Full Name',
dataTestId: 'name',
},
];
if (BILLING_ENABLED) {
formFields = formFields.concat([
{
field: {
companyName: true,
},
fieldType: FormFieldSchemaType.Text,
placeholder: 'Acme, Inc.',
required: true,
title: 'Company Name',
dataTestId: 'companyName',
},
]);
// If reseller wants to hide phone number on sign up, we hide it.
if (!reseller || !reseller.hidePhoneNumberOnSignup) {
formFields.push({
field: {
companyPhoneNumber: true,
},
fieldType: FormFieldSchemaType.Phone,
required: true,
placeholder: '+11234567890',
title: 'Phone Number',
dataTestId: 'companyPhoneNumber',
});
}
} }
if (Navigation.getQueryStringByName("email")) {
setInitialValues({
email: Navigation.getQueryStringByName("email"),
});
}
// if promo code is found, please save it in localstorage.
if (Navigation.getQueryStringByName("partnerId")) {
await fetchReseller(Navigation.getQueryStringByName("partnerId")!);
}
}, []);
let formFields: Fields<User> = [
{
field: {
email: true,
},
fieldType: FormFieldSchemaType.Email,
placeholder: "jeff@example.com",
required: true,
disabled: Boolean(initialValues && initialValues["email"]),
title: "Email",
dataTestId: "email",
},
{
field: {
name: true,
},
fieldType: FormFieldSchemaType.Text,
placeholder: "Jeff Smith",
required: true,
title: "Full Name",
dataTestId: "name",
},
];
if (BILLING_ENABLED) {
formFields = formFields.concat([ formFields = formFields.concat([
{ {
field: { field: {
password: true, companyName: true,
},
fieldType: FormFieldSchemaType.Password,
validation: {
minLength: 6,
},
placeholder: 'Password',
title: 'Password',
required: true,
dataTestId: 'password',
},
{
field: {
confirmPassword: true,
} as any,
validation: {
minLength: 6,
toMatchField: 'password',
},
fieldType: FormFieldSchemaType.Password,
placeholder: 'Confirm Password',
title: 'Confirm Password',
overrideFieldKey: 'confirmPassword',
required: true,
showEvenIfPermissionDoesNotExist: true,
dataTestId: 'confirmPassword',
}, },
fieldType: FormFieldSchemaType.Text,
placeholder: "Acme, Inc.",
required: true,
title: "Company Name",
dataTestId: "companyName",
},
]); ]);
if (error) { // If reseller wants to hide phone number on sign up, we hide it.
return <ErrorMessage error={error} />; if (!reseller || !reseller.hidePhoneNumberOnSignup) {
formFields.push({
field: {
companyPhoneNumber: true,
},
fieldType: FormFieldSchemaType.Phone,
required: true,
placeholder: "+11234567890",
title: "Phone Number",
dataTestId: "companyPhoneNumber",
});
} }
}
if (isLoading) { formFields = formFields.concat([
return <PageLoader isVisible={true} />; {
} field: {
password: true,
},
fieldType: FormFieldSchemaType.Password,
validation: {
minLength: 6,
},
placeholder: "Password",
title: "Password",
required: true,
dataTestId: "password",
},
{
field: {
confirmPassword: true,
} as any,
validation: {
minLength: 6,
toMatchField: "password",
},
fieldType: FormFieldSchemaType.Password,
placeholder: "Confirm Password",
title: "Confirm Password",
overrideFieldKey: "confirmPassword",
required: true,
showEvenIfPermissionDoesNotExist: true,
dataTestId: "confirmPassword",
},
]);
return ( if (error) {
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8"> return <ErrorMessage error={error} />;
<div className="sm:mx-auto sm:w-full sm:max-w-md"> }
<img
className="mx-auto h-12 w-auto"
src={OneUptimeLogo}
alt="OneUptime"
/>
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
Create your OneUptime account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Join thousands of business that use OneUptime to help them
stay online all the time.
</p>
<p className="mt-2 text-center text-sm text-gray-600">
No credit card required.
</p>
</div>
<div className="mt-8 lg:mx-auto lg:w-full lg:max-w-2xl"> if (isLoading) {
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> return <PageLoader isVisible={true} />;
<ModelForm<User> }
modelType={User}
id="register-form"
showAsColumns={reseller ? 1 : 2}
name="Register"
initialValues={initialValues}
maxPrimaryButtonWidth={true}
fields={formFields}
createOrUpdateApiUrl={apiUrl}
onBeforeCreate={(item: User): Promise<User> => {
const utmParams: Dictionary<string> =
UserUtil.getUtmParams();
if ( return (
utmParams && <div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
Object.keys(utmParams).length > 0 <div className="sm:mx-auto sm:w-full sm:max-w-md">
) { <img
item.utmSource = utmParams['utmSource'] || ''; className="mx-auto h-12 w-auto"
item.utmMedium = utmParams['utmMedium'] || ''; src={OneUptimeLogo}
item.utmCampaign = alt="OneUptime"
utmParams['utmCampaign'] || ''; />
item.utmTerm = utmParams['utmTerm'] || ''; <h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
item.utmContent = utmParams['utmContent'] || ''; Create your OneUptime account
item.utmUrl = utmParams['utmUrl'] || ''; </h2>
<p className="mt-2 text-center text-sm text-gray-600">
Join thousands of business that use OneUptime to help them stay online
all the time.
</p>
<p className="mt-2 text-center text-sm text-gray-600">
No credit card required.
</p>
</div>
UiAnalytics.capture('utm_event', utmParams); <div className="mt-8 lg:mx-auto lg:w-full lg:max-w-2xl">
} <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<ModelForm<User>
modelType={User}
id="register-form"
showAsColumns={reseller ? 1 : 2}
name="Register"
initialValues={initialValues}
maxPrimaryButtonWidth={true}
fields={formFields}
createOrUpdateApiUrl={apiUrl}
onBeforeCreate={(item: User): Promise<User> => {
const utmParams: Dictionary<string> = UserUtil.getUtmParams();
return Promise.resolve(item); if (utmParams && Object.keys(utmParams).length > 0) {
}} item.utmSource = utmParams["utmSource"] || "";
formType={FormType.Create} item.utmMedium = utmParams["utmMedium"] || "";
submitButtonText={'Sign Up'} item.utmCampaign = utmParams["utmCampaign"] || "";
onSuccess={( item.utmTerm = utmParams["utmTerm"] || "";
value: User, item.utmContent = utmParams["utmContent"] || "";
miscData: JSONObject | undefined item.utmUrl = utmParams["utmUrl"] || "";
) => {
if (value && value.email) {
UiAnalytics.userAuth(value.email);
UiAnalytics.capture('accounts/register');
}
LoginUtil.login({ UiAnalytics.capture("utm_event", utmParams);
user: value, }
token: miscData ? miscData['token'] : undefined,
}); return Promise.resolve(item);
}} }}
/> formType={FormType.Create}
</div> submitButtonText={"Sign Up"}
<div className="mt-5 text-center text-gray-500"> onSuccess={(value: User, miscData: JSONObject | undefined) => {
<p className="text-muted mb-0"> if (value && value.email) {
Already have an account?{' '} UiAnalytics.userAuth(value.email);
<Link UiAnalytics.capture("accounts/register");
to={new Route('/accounts/login')} }
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
> LoginUtil.login({
Log in. user: value,
</Link> token: miscData ? miscData["token"] : undefined,
</p> });
</div> }}
</div> />
</div> </div>
); <div className="mt-5 text-center text-gray-500">
<p className="text-muted mb-0">
Already have an account?{" "}
<Link
to={new Route("/accounts/login")}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
>
Log in.
</Link>
</p>
</div>
</div>
</div>
);
}; };
export default RegisterPage; export default RegisterPage;

View File

@@ -1,115 +1,114 @@
import { RESET_PASSWORD_API_URL } from '../Utils/ApiPaths'; import { RESET_PASSWORD_API_URL } from "../Utils/ApiPaths";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import ModelForm, { FormType } from 'CommonUI/src/Components/Forms/ModelForm'; import ModelForm, { FormType } from "CommonUI/src/Components/Forms/ModelForm";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import Link from 'CommonUI/src/Components/Link/Link'; import Link from "CommonUI/src/Components/Link/Link";
import OneUptimeLogo from 'CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg'; import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import User from 'Model/Models/User'; import User from "Model/Models/User";
import React, { useState } from 'react'; import React, { useState } from "react";
const RegisterPage: () => JSX.Element = () => { const RegisterPage: () => JSX.Element = () => {
const apiUrl: URL = RESET_PASSWORD_API_URL; const apiUrl: URL = RESET_PASSWORD_API_URL;
const [isSuccess, setIsSuccess] = useState<boolean>(false); const [isSuccess, setIsSuccess] = useState<boolean>(false);
return ( return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8"> <div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md"> <div className="sm:mx-auto sm:w-full sm:max-w-md">
<img <img
className="mx-auto h-12 w-auto" className="mx-auto h-12 w-auto"
src={OneUptimeLogo} src={OneUptimeLogo}
alt="Your Company" alt="Your Company"
/> />
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900"> <h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
Reset your password Reset your password
</h2> </h2>
{!isSuccess && ( {!isSuccess && (
<p className="mt-2 text-center text-sm text-gray-600"> <p className="mt-2 text-center text-sm text-gray-600">
Please enter your new password and we will have it Please enter your new password and we will have it updated.{" "}
updated.{' '} </p>
</p> )}
)}
{isSuccess && ( {isSuccess && (
<p className="mt-2 text-center text-sm text-gray-600"> <p className="mt-2 text-center text-sm text-gray-600">
Your password has been updated. Please log in. Your password has been updated. Please log in.
</p> </p>
)} )}
</div> </div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
{!isSuccess && ( {!isSuccess && (
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<ModelForm<User> <ModelForm<User>
modelType={User} modelType={User}
id="register-form" id="register-form"
name="Reset Password" name="Reset Password"
onBeforeCreate={(item: User): Promise<User> => { onBeforeCreate={(item: User): Promise<User> => {
item.resetPasswordToken = item.resetPasswordToken =
Navigation.getLastParam() Navigation.getLastParam()
?.toString() ?.toString()
.replace('/', '') .replace("/", "")
.toString() || ''; .toString() || "";
return Promise.resolve(item); return Promise.resolve(item);
}} }}
showAsColumns={1} showAsColumns={1}
maxPrimaryButtonWidth={true} maxPrimaryButtonWidth={true}
fields={[ fields={[
{ {
field: { field: {
password: true, password: true,
}, },
fieldType: FormFieldSchemaType.Password, fieldType: FormFieldSchemaType.Password,
validation: { validation: {
minLength: 6, minLength: 6,
}, },
placeholder: 'New Password', placeholder: "New Password",
title: 'New Password', title: "New Password",
required: true, required: true,
showEvenIfPermissionDoesNotExist: true, showEvenIfPermissionDoesNotExist: true,
}, },
{ {
field: { field: {
confirmPassword: true, confirmPassword: true,
} as any, } as any,
validation: { validation: {
minLength: 6, minLength: 6,
toMatchField: 'password', toMatchField: "password",
}, },
fieldType: FormFieldSchemaType.Password, fieldType: FormFieldSchemaType.Password,
placeholder: 'Confirm Password', placeholder: "Confirm Password",
title: 'Confirm Password', title: "Confirm Password",
overrideFieldKey: 'confirmPassword', overrideFieldKey: "confirmPassword",
required: true, required: true,
showEvenIfPermissionDoesNotExist: true, showEvenIfPermissionDoesNotExist: true,
}, },
]} ]}
createOrUpdateApiUrl={apiUrl} createOrUpdateApiUrl={apiUrl}
formType={FormType.Create} formType={FormType.Create}
submitButtonText={'Reset Password'} submitButtonText={"Reset Password"}
onSuccess={() => { onSuccess={() => {
setIsSuccess(true); setIsSuccess(true);
}} }}
/> />
</div> </div>
)} )}
<div className="mt-5 text-center"> <div className="mt-5 text-center">
<p className="text-muted mb-0 text-gray-500"> <p className="text-muted mb-0 text-gray-500">
Know your password?{' '} Know your password?{" "}
<Link <Link
to={new Route('/accounts/login')} to={new Route("/accounts/login")}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer" className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
> >
Log in. Log in.
</Link> </Link>
</p> </p>
</div>
</div>
</div> </div>
); </div>
</div>
);
}; };
export default RegisterPage; export default RegisterPage;

View File

@@ -1,132 +1,121 @@
import { VERIFY_EMAIL_API_URL } from '../Utils/ApiPaths'; import { VERIFY_EMAIL_API_URL } from "../Utils/ApiPaths";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes'; import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import { FormType } from 'CommonUI/src/Components/Forms/ModelForm'; import { FormType } from "CommonUI/src/Components/Forms/ModelForm";
import Link from 'CommonUI/src/Components/Link/Link'; import Link from "CommonUI/src/Components/Link/Link";
import PageLoader from 'CommonUI/src/Components/Loader/PageLoader'; import PageLoader from "CommonUI/src/Components/Loader/PageLoader";
import OneUptimeLogo from 'CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg'; import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
import API from 'CommonUI/src/Utils/API/API'; import API from "CommonUI/src/Utils/API/API";
import ModelAPI from 'CommonUI/src/Utils/ModelAPI/ModelAPI'; import ModelAPI from "CommonUI/src/Utils/ModelAPI/ModelAPI";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import EmailVerificationToken from 'Model/Models/EmailVerificationToken'; import EmailVerificationToken from "Model/Models/EmailVerificationToken";
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from "react";
const VerifyEmail: () => JSX.Element = () => { const VerifyEmail: () => JSX.Element = () => {
const apiUrl: URL = VERIFY_EMAIL_API_URL; const apiUrl: URL = VERIFY_EMAIL_API_URL;
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const init: PromiseVoidFunction = async (): Promise<void> => { const init: PromiseVoidFunction = async (): Promise<void> => {
// Ping an API here. // Ping an API here.
setError(''); setError("");
setIsLoading(true); setIsLoading(true);
try { try {
// strip data. // strip data.
const emailverificationToken: EmailVerificationToken = const emailverificationToken: EmailVerificationToken =
new EmailVerificationToken(); new EmailVerificationToken();
emailverificationToken.token = new ObjectID( emailverificationToken.token = new ObjectID(
Navigation.getLastParam()?.toString().replace('/', '') || '' Navigation.getLastParam()?.toString().replace("/", "") || "",
); );
await ModelAPI.createOrUpdate<EmailVerificationToken>({ await ModelAPI.createOrUpdate<EmailVerificationToken>({
model: emailverificationToken, model: emailverificationToken,
modelType: EmailVerificationToken, modelType: EmailVerificationToken,
formType: FormType.Create, formType: FormType.Create,
miscDataProps: {}, miscDataProps: {},
requestOptions: { requestOptions: {
overrideRequestUrl: apiUrl, overrideRequestUrl: apiUrl,
}, },
}); });
} catch (err) { } catch (err) {
setError(API.getFriendlyMessage(err)); setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
init().catch((err: Error) => {
setError(err.toString());
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
} }
return ( setIsLoading(false);
<div className="auth-page"> };
<div className="container-fluid p-0">
<div className="row g-0">
<div className="col-xxl-4 col-lg-4 col-md-3"></div>
<div className="col-xxl-4 col-lg-4 col-md-6"> useEffect(() => {
<div className="auth-full-page-content d-flex p-sm-5 p-4"> init().catch((err: Error) => {
<div className="w-100"> setError(err.toString());
<div className="d-flex flex-column h-100"> });
<div className="auth-content my-auto"> }, []);
<div
className="mt-4 text-center flex justify-center"
style={{ marginBottom: '40px' }}
>
<img
style={{ height: '50px' }}
src={`${OneUptimeLogo}`}
/>
</div>
{!error && (
<div className="text-center">
<h5 className="mb-0">
Your email is verified.
</h5>
<p className="text-muted mt-2 mb-0">
Thank you for verifying your
email. You can now log in to
OneUptime.{' '}
</p>
</div>
)}
{error && ( if (isLoading) {
<div className="text-center"> return <PageLoader isVisible={true} />;
<h5 className="mb-0"> }
Sorry, something went wrong!
</h5>
<p className="text-muted mt-2 mb-0">
{error}
</p>
</div>
)}
<div className="mt-5 text-center"> return (
<p className="text-muted mb-0"> <div className="auth-page">
Return to sign in?{' '} <div className="container-fluid p-0">
<Link <div className="row g-0">
to={ <div className="col-xxl-4 col-lg-4 col-md-3"></div>
new Route(
'/accounts/login' <div className="col-xxl-4 col-lg-4 col-md-6">
) <div className="auth-full-page-content d-flex p-sm-5 p-4">
} <div className="w-100">
className="hover:underline text-primary fw-semibold" <div className="d-flex flex-column h-100">
> <div className="auth-content my-auto">
Login. <div
</Link> className="mt-4 text-center flex justify-center"
</p> style={{ marginBottom: "40px" }}
</div> >
</div> <img
</div> style={{ height: "50px" }}
</div> src={`${OneUptimeLogo}`}
</div> />
</div> </div>
{!error && (
<div className="text-center">
<h5 className="mb-0">Your email is verified.</h5>
<p className="text-muted mt-2 mb-0">
Thank you for verifying your email. You can now log in
to OneUptime.{" "}
</p>
</div>
)}
<div className="col-xxl-4 col-lg-4 col-md-3"></div> {error && (
<div className="text-center">
<h5 className="mb-0">Sorry, something went wrong!</h5>
<p className="text-muted mt-2 mb-0">{error}</p>
</div>
)}
<div className="mt-5 text-center">
<p className="text-muted mb-0">
Return to sign in?{" "}
<Link
to={new Route("/accounts/login")}
className="hover:underline text-primary fw-semibold"
>
Login.
</Link>
</p>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div>
<div className="col-xxl-4 col-lg-4 col-md-3"></div>
</div> </div>
); </div>
</div>
);
}; };
export default VerifyEmail; export default VerifyEmail;

View File

@@ -1,22 +1,22 @@
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import { IDENTITY_URL } from 'CommonUI/src/Config'; import { IDENTITY_URL } from "CommonUI/src/Config";
export const SIGNUP_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute( export const SIGNUP_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route('/signup') new Route("/signup"),
); );
export const LOGIN_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute( export const LOGIN_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route('/login') new Route("/login"),
); );
export const FORGOT_PASSWORD_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute( export const FORGOT_PASSWORD_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route('/forgot-password') new Route("/forgot-password"),
); );
export const VERIFY_EMAIL_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute( export const VERIFY_EMAIL_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route('/verify-email') new Route("/verify-email"),
); );
export const RESET_PASSWORD_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute( export const RESET_PASSWORD_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route('/reset-password') new Route("/reset-password"),
); );

View File

@@ -1,83 +1,84 @@
require('ts-loader'); require("ts-loader");
require('file-loader'); require("file-loader");
require('style-loader'); require("style-loader");
require('css-loader'); require("css-loader");
require('sass-loader'); require("sass-loader");
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const dotenv = require('dotenv'); const dotenv = require("dotenv");
const express = require('express'); const express = require("express");
const readEnvFile = (pathToFile) => { const readEnvFile = (pathToFile) => {
const parsed = dotenv.config({ path: pathToFile }).parsed;
const parsed = dotenv.config({ path: pathToFile }).parsed; const env = {};
const env = { for (const key in parsed) {
}; env[key] = JSON.stringify(parsed[key]);
}
for (const key in parsed) { return env;
env[key] = JSON.stringify(parsed[key]); };
}
return env;
}
module.exports = { module.exports = {
entry: "./src/Index.tsx", entry: "./src/Index.tsx",
mode: "development", mode: "development",
output: { output: {
filename: "bundle.js", filename: "bundle.js",
path: path.resolve(__dirname, "public", "dist"), path: path.resolve(__dirname, "public", "dist"),
publicPath: "/accounts/dist/", publicPath: "/accounts/dist/",
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".scss"],
alias: {
react: path.resolve("./node_modules/react"),
}, },
resolve: { },
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.css', '.scss'], externals: {
alias: { "react-native-sqlite-storage": "react-native-sqlite-storage",
react: path.resolve('./node_modules/react'), },
} plugins: [
}, new webpack.DefinePlugin({
externals: { process: {
'react-native-sqlite-storage': 'react-native-sqlite-storage' env: {
}, ...readEnvFile("/usr/src/app/dev-env/.env"),
plugins: [
new webpack.DefinePlugin({
'process': {
'env': {
...readEnvFile('/usr/src/app/dev-env/.env')
}
}
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader'
},
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', "sass-loader"]
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loader: 'file-loader'
}
],
},
devServer: {
historyApiFallback: true,
devMiddleware: {
writeToDisk: true,
}, },
allowedHosts: "all", },
setupMiddlewares: (middlewares, devServer) => { }),
devServer.app.use('/accounts/assets', express.static(path.resolve(__dirname, 'public', 'assets'))); ],
return middlewares; module: {
} rules: [
{
test: /\.(ts|tsx)$/,
use: "ts-loader",
},
{
test: /\.s[ac]ss$/i,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loader: "file-loader",
},
],
},
devServer: {
historyApiFallback: true,
devMiddleware: {
writeToDisk: true,
}, },
devtool: 'eval-source-map', allowedHosts: "all",
} setupMiddlewares: (middlewares, devServer) => {
devServer.app.use(
"/accounts/assets",
express.static(path.resolve(__dirname, "public", "assets")),
);
return middlewares;
},
},
devtool: "eval-source-map",
};

View File

@@ -1,38 +1,38 @@
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes'; import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import Express, { ExpressApplication } from 'CommonServer/Utils/Express'; import Express, { ExpressApplication } from "CommonServer/Utils/Express";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import App from 'CommonServer/Utils/StartServer'; import App from "CommonServer/Utils/StartServer";
export const APP_NAME: string = 'admin'; export const APP_NAME: string = "admin";
const app: ExpressApplication = Express.getExpressApp(); const app: ExpressApplication = Express.getExpressApp();
const init: PromiseVoidFunction = async (): Promise<void> => { const init: PromiseVoidFunction = async (): Promise<void> => {
try { try {
// init the app // init the app
await App.init({ await App.init({
appName: APP_NAME, appName: APP_NAME,
port: undefined, port: undefined,
isFrontendApp: true, isFrontendApp: true,
statusOptions: { statusOptions: {
liveCheck: async () => {}, liveCheck: async () => {},
readyCheck: async () => {}, readyCheck: async () => {},
}, },
}); });
// add default routes // add default routes
await App.addDefaultRoutes(); await App.addDefaultRoutes();
} catch (err) { } catch (err) {
logger.error('App Init Failed:'); logger.error("App Init Failed:");
logger.error(err); logger.error(err);
throw err; throw err;
} }
}; };
init().catch((err: Error) => { init().catch((err: Error) => {
logger.error(err); logger.error(err);
logger.error('Exiting node process'); logger.error("Exiting node process");
process.exit(1); process.exit(1);
}); });
export default app; export default app;

View File

@@ -1,4 +1,4 @@
declare module '*.png'; declare module "*.png";
declare module '*.svg'; declare module "*.svg";
declare module '*.jpg'; declare module "*.jpg";
declare module '*.gif'; declare module "*.gif";

View File

@@ -1,112 +1,103 @@
import MasterPage from './Components/MasterPage/MasterPage'; import MasterPage from "./Components/MasterPage/MasterPage";
import Init from './Pages/Init/Init'; import Init from "./Pages/Init/Init";
import Logout from './Pages/Logout/Logout'; import Logout from "./Pages/Logout/Logout";
import Projects from './Pages/Projects/Index'; import Projects from "./Pages/Projects/Index";
import SettingsAPIKey from './Pages/Settings/APIKey/Index'; import SettingsAPIKey from "./Pages/Settings/APIKey/Index";
import SettingsAuthentication from './Pages/Settings/Authentication/Index'; import SettingsAuthentication from "./Pages/Settings/Authentication/Index";
import SettingsCallSMS from './Pages/Settings/CallSMS/Index'; import SettingsCallSMS from "./Pages/Settings/CallSMS/Index";
// Settings Pages. // Settings Pages.
import SettingsEmail from './Pages/Settings/Email/Index'; import SettingsEmail from "./Pages/Settings/Email/Index";
import SettingsProbes from './Pages/Settings/Probes/Index'; import SettingsProbes from "./Pages/Settings/Probes/Index";
import Users from './Pages/Users/Index'; import Users from "./Pages/Users/Index";
import PageMap from './Utils/PageMap'; import PageMap from "./Utils/PageMap";
import RouteMap from './Utils/RouteMap'; import RouteMap from "./Utils/RouteMap";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import { ACCOUNTS_URL, DASHBOARD_URL } from 'CommonUI/src/Config'; import { ACCOUNTS_URL, DASHBOARD_URL } from "CommonUI/src/Config";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import User from 'CommonUI/src/Utils/User'; import User from "CommonUI/src/Utils/User";
import React from 'react'; import React from "react";
import { import {
Route as PageRoute, Route as PageRoute,
Routes, Routes,
useLocation, useLocation,
useNavigate, useNavigate,
useParams, useParams,
} from 'react-router-dom'; } from "react-router-dom";
const App: () => JSX.Element = () => { const App: () => JSX.Element = () => {
Navigation.setNavigateHook(useNavigate()); Navigation.setNavigateHook(useNavigate());
Navigation.setLocation(useLocation()); Navigation.setLocation(useLocation());
Navigation.setParams(useParams()); Navigation.setParams(useParams());
if (!User.isLoggedIn()) { if (!User.isLoggedIn()) {
if (Navigation.getQueryStringByName('sso_token')) { if (Navigation.getQueryStringByName("sso_token")) {
Navigation.navigate( Navigation.navigate(
URL.fromString(ACCOUNTS_URL.toString()).addQueryParam( URL.fromString(ACCOUNTS_URL.toString()).addQueryParam("sso", "true"),
'sso', );
'true' } else {
) Navigation.navigate(URL.fromString(ACCOUNTS_URL.toString()));
);
} else {
Navigation.navigate(URL.fromString(ACCOUNTS_URL.toString()));
}
} }
}
if (!User.isMasterAdmin()) { if (!User.isMasterAdmin()) {
Navigation.navigate(URL.fromString(DASHBOARD_URL.toString())); Navigation.navigate(URL.fromString(DASHBOARD_URL.toString()));
} }
return ( return (
<MasterPage> <MasterPage>
<Routes> <Routes>
<PageRoute <PageRoute
path={RouteMap[PageMap.INIT]?.toString() || ''} path={RouteMap[PageMap.INIT]?.toString() || ""}
element={<Init />} element={<Init />}
/> />
<PageRoute <PageRoute
path={RouteMap[PageMap.PROJECTS]?.toString() || ''} path={RouteMap[PageMap.PROJECTS]?.toString() || ""}
element={<Projects />} element={<Projects />}
/> />
<PageRoute <PageRoute
path={RouteMap[PageMap.USERS]?.toString() || ''} path={RouteMap[PageMap.USERS]?.toString() || ""}
element={<Users />} element={<Users />}
/> />
<PageRoute <PageRoute
path={RouteMap[PageMap.LOGOUT]?.toString() || ''} path={RouteMap[PageMap.LOGOUT]?.toString() || ""}
element={<Logout />} element={<Logout />}
/> />
<PageRoute <PageRoute
path={RouteMap[PageMap.SETTINGS]?.toString() || ''} path={RouteMap[PageMap.SETTINGS]?.toString() || ""}
element={<SettingsAuthentication />} element={<SettingsAuthentication />}
/> />
<PageRoute <PageRoute
path={RouteMap[PageMap.SETTINGS_SMTP]?.toString() || ''} path={RouteMap[PageMap.SETTINGS_SMTP]?.toString() || ""}
element={<SettingsEmail />} element={<SettingsEmail />}
/> />
<PageRoute <PageRoute
path={ path={RouteMap[PageMap.SETTINGS_CALL_AND_SMS]?.toString() || ""}
RouteMap[PageMap.SETTINGS_CALL_AND_SMS]?.toString() || element={<SettingsCallSMS />}
'' />
}
element={<SettingsCallSMS />}
/>
<PageRoute <PageRoute
path={RouteMap[PageMap.SETTINGS_PROBES]?.toString() || ''} path={RouteMap[PageMap.SETTINGS_PROBES]?.toString() || ""}
element={<SettingsProbes />} element={<SettingsProbes />}
/> />
<PageRoute <PageRoute
path={ path={RouteMap[PageMap.SETTINGS_AUTHENTICATION]?.toString() || ""}
RouteMap[PageMap.SETTINGS_AUTHENTICATION]?.toString() || element={<SettingsAuthentication />}
'' />
}
element={<SettingsAuthentication />}
/>
<PageRoute <PageRoute
path={RouteMap[PageMap.SETTINGS_API_KEY]?.toString() || ''} path={RouteMap[PageMap.SETTINGS_API_KEY]?.toString() || ""}
element={<SettingsAPIKey />} element={<SettingsAPIKey />}
/> />
</Routes> </Routes>
</MasterPage> </MasterPage>
); );
}; };
export default App; export default App;

View File

@@ -1,122 +1,114 @@
import HTTPResponse from 'Common/Types/API/HTTPResponse'; import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import Dictionary from 'Common/Types/Dictionary'; import Dictionary from "Common/Types/Dictionary";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes'; import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import API from 'Common/Utils/API'; import API from "Common/Utils/API";
import Footer from 'CommonUI/src/Components/Footer/Footer'; import Footer from "CommonUI/src/Components/Footer/Footer";
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal'; import ConfirmModal from "CommonUI/src/Components/Modal/ConfirmModal";
import { HOST, HTTP_PROTOCOL } from 'CommonUI/src/Config'; import { HOST, HTTP_PROTOCOL } from "CommonUI/src/Config";
import React from 'react'; import React from "react";
const DashboardFooter: () => JSX.Element = () => { const DashboardFooter: () => JSX.Element = () => {
const [showAboutModal, setShowAboutModal] = React.useState<boolean>(false); const [showAboutModal, setShowAboutModal] = React.useState<boolean>(false);
const [isAboutModalLoading, setIsAboutModalLoading] = const [isAboutModalLoading, setIsAboutModalLoading] =
React.useState<boolean>(false); React.useState<boolean>(false);
const [versionText, setVersionText] = React.useState<Dictionary<string>>( const [versionText, setVersionText] = React.useState<Dictionary<string>>({});
{}
const fetchVersions: PromiseVoidFunction = async (): Promise<void> => {
setIsAboutModalLoading(true);
try {
const verText: Dictionary<string> = {};
const apps: Array<{
name: string;
path: string;
}> = [
{
name: "API",
path: "/api",
},
{
name: "Dashboard",
path: "/dashboard",
},
];
for (const app of apps) {
const version: JSONObject = await fetchAppVersion(app.path);
verText[app.name] =
`${app.name}: ${version["version"]} (${version["commit"]})`;
}
setVersionText(verText);
} catch (err) {
setVersionText({
error: "Version data is not available: " + (err as Error).message,
});
}
setIsAboutModalLoading(false);
};
const fetchAppVersion: (appName: string) => Promise<JSONObject> = async (
appName: string,
): Promise<JSONObject> => {
const response: HTTPResponse<JSONObject> = await API.get<JSONObject>(
URL.fromString(`${HTTP_PROTOCOL}/${HOST}${appName}/version`),
); );
const fetchVersions: PromiseVoidFunction = async (): Promise<void> => { if (response.data) {
setIsAboutModalLoading(true); return response.data as JSONObject;
}
throw new BadDataException("Version data is not available");
};
try { return (
const verText: Dictionary<string> = {}; <>
const apps: Array<{ <Footer
name: string; className="bg-white h-16 inset-x-0 bottom-0 px-8"
path: string; copyright="HackerBay, Inc."
}> = [ links={[
{ {
name: 'API', title: "Help and Support",
path: '/api', to: URL.fromString("https://oneuptime.com/support"),
}, },
{ {
name: 'Dashboard', title: "Legal",
path: '/dashboard', to: URL.fromString("https://oneuptime.com/legal"),
}, },
]; {
title: "Version",
onClick: async () => {
setShowAboutModal(true);
await fetchVersions();
},
},
]}
/>
for (const app of apps) { {showAboutModal ? (
const version: JSONObject = await fetchAppVersion(app.path); <ConfirmModal
verText[ title={`OneUptime Version`}
app.name description={
] = `${app.name}: ${version['version']} (${version['commit']})`; <div>
} {Object.keys(versionText).map((key: string, i: number) => {
return <div key={i}>{versionText[key]}</div>;
setVersionText(verText); })}
} catch (err) { </div>
setVersionText({ }
error: isLoading={isAboutModalLoading}
'Version data is not available: ' + (err as Error).message, submitButtonText={"Close"}
}); onSubmit={() => {
} return setShowAboutModal(false);
}}
setIsAboutModalLoading(false); />
}; ) : (
<></>
const fetchAppVersion: (appName: string) => Promise<JSONObject> = async ( )}
appName: string </>
): Promise<JSONObject> => { );
const response: HTTPResponse<JSONObject> = await API.get<JSONObject>(
URL.fromString(`${HTTP_PROTOCOL}/${HOST}${appName}/version`)
);
if (response.data) {
return response.data as JSONObject;
}
throw new BadDataException('Version data is not available');
};
return (
<>
<Footer
className="bg-white h-16 inset-x-0 bottom-0 px-8"
copyright="HackerBay, Inc."
links={[
{
title: 'Help and Support',
to: URL.fromString('https://oneuptime.com/support'),
},
{
title: 'Legal',
to: URL.fromString('https://oneuptime.com/legal'),
},
{
title: 'Version',
onClick: async () => {
setShowAboutModal(true);
await fetchVersions();
},
},
]}
/>
{showAboutModal ? (
<ConfirmModal
title={`OneUptime Version`}
description={
<div>
{Object.keys(versionText).map(
(key: string, i: number) => {
return (
<div key={i}>{versionText[key]}</div>
);
}
)}
</div>
}
isLoading={isAboutModalLoading}
submitButtonText={'Close'}
onSubmit={() => {
return setShowAboutModal(false);
}}
/>
) : (
<></>
)}
</>
);
}; };
export default DashboardFooter; export default DashboardFooter;

View File

@@ -1,46 +1,46 @@
import Help from './Help'; import Help from "./Help";
import Logo from './Logo'; import Logo from "./Logo";
import UserProfile from './UserProfile'; import UserProfile from "./UserProfile";
import Button, { ButtonStyleType } from 'CommonUI/src/Components/Button/Button'; import Button, { ButtonStyleType } from "CommonUI/src/Components/Button/Button";
import Header from 'CommonUI/src/Components/Header/Header'; import Header from "CommonUI/src/Components/Header/Header";
import { DASHBOARD_URL } from 'CommonUI/src/Config'; import { DASHBOARD_URL } from "CommonUI/src/Config";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import React, { FunctionComponent, ReactElement } from 'react'; import React, { FunctionComponent, ReactElement } from "react";
const DashboardHeader: FunctionComponent = (): ReactElement => { const DashboardHeader: FunctionComponent = (): ReactElement => {
return ( return (
<> <>
<Header <Header
leftComponents={ leftComponents={
<> <>
<Logo onClick={() => {}} /> <Logo onClick={() => {}} />
</> </>
} }
centerComponents={ centerComponents={
<> <>
{/* <SearchBox {/* <SearchBox
key={2} key={2}
selectedProject={props.selectedProject} selectedProject={props.selectedProject}
onChange={(_value: string) => { }} onChange={(_value: string) => { }}
/>{' '} */} />{' '} */}
</> </>
} }
rightComponents={ rightComponents={
<> <>
<Button <Button
title="Exit Admin" title="Exit Admin"
buttonStyle={ButtonStyleType.NORMAL} buttonStyle={ButtonStyleType.NORMAL}
onClick={() => { onClick={() => {
Navigation.navigate(DASHBOARD_URL); Navigation.navigate(DASHBOARD_URL);
}} }}
/>
<Help />
<UserProfile />
</>
}
/> />
</> <Help />
); <UserProfile />
</>
}
/>
</>
);
}; };
export default DashboardHeader; export default DashboardHeader;

View File

@@ -1,60 +1,58 @@
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import IconProp from 'Common/Types/Icon/IconProp'; import IconProp from "Common/Types/Icon/IconProp";
import HeaderIconDropdownButton from 'CommonUI/src/Components/Header/HeaderIconDropdownButton'; import HeaderIconDropdownButton from "CommonUI/src/Components/Header/HeaderIconDropdownButton";
import IconDropdownItem from 'CommonUI/src/Components/Header/IconDropdown/IconDropdownItem'; import IconDropdownItem from "CommonUI/src/Components/Header/IconDropdown/IconDropdownItem";
import IconDropdownMenu from 'CommonUI/src/Components/Header/IconDropdown/IconDropdownMenu'; import IconDropdownMenu from "CommonUI/src/Components/Header/IconDropdown/IconDropdownMenu";
import IconDropdownRow from 'CommonUI/src/Components/Header/IconDropdown/IconDropdownRow'; import IconDropdownRow from "CommonUI/src/Components/Header/IconDropdown/IconDropdownRow";
import React, { ReactElement, useState } from 'react'; import React, { ReactElement, useState } from "react";
const Help: () => JSX.Element = (): ReactElement => { const Help: () => JSX.Element = (): ReactElement => {
const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false); const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
return ( return (
<HeaderIconDropdownButton <HeaderIconDropdownButton
icon={IconProp.Help} icon={IconProp.Help}
name="Help" name="Help"
showDropdown={isDropdownVisible} showDropdown={isDropdownVisible}
onClick={() => {
setIsDropdownVisible(true);
}}
>
<IconDropdownMenu>
<IconDropdownRow>
<IconDropdownItem
title="Support Email"
icon={IconProp.Email}
openInNewTab={true}
url={URL.fromString("mailto:support@oneuptime.com")}
onClick={() => { onClick={() => {
setIsDropdownVisible(true); setIsDropdownVisible(false);
}} }}
> />
<IconDropdownMenu> <IconDropdownItem
<IconDropdownRow> title="Chat on Slack"
<IconDropdownItem icon={IconProp.Slack}
title="Support Email" openInNewTab={true}
icon={IconProp.Email} onClick={() => {
openInNewTab={true} setIsDropdownVisible(false);
url={URL.fromString('mailto:support@oneuptime.com')} }}
onClick={() => { url={URL.fromString(
setIsDropdownVisible(false); "https://join.slack.com/t/oneuptimesupport/shared_invite/zt-1kavkds2f-gegm_wePorvwvM3M_SaoCQ",
}} )}
/> />
<IconDropdownItem <IconDropdownItem
title="Chat on Slack" title="Request Demo"
icon={IconProp.Slack} icon={IconProp.Window}
openInNewTab={true} onClick={() => {
onClick={() => { setIsDropdownVisible(false);
setIsDropdownVisible(false); }}
}} openInNewTab={true}
url={URL.fromString( url={URL.fromString("https://oneuptime.com/enterprise/demo")}
'https://join.slack.com/t/oneuptimesupport/shared_invite/zt-1kavkds2f-gegm_wePorvwvM3M_SaoCQ' />
)} </IconDropdownRow>
/> </IconDropdownMenu>
<IconDropdownItem </HeaderIconDropdownButton>
title="Request Demo" );
icon={IconProp.Window}
onClick={() => {
setIsDropdownVisible(false);
}}
openInNewTab={true}
url={URL.fromString(
'https://oneuptime.com/enterprise/demo'
)}
/>
</IconDropdownRow>
</IconDropdownMenu>
</HeaderIconDropdownButton>
);
}; };
export default Help; export default Help;

View File

@@ -1,30 +1,30 @@
// Tailwind // Tailwind
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import Image from 'CommonUI/src/Components/Image/Image'; import Image from "CommonUI/src/Components/Image/Image";
import OneUptimeLogo from 'CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg'; import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
import React, { FunctionComponent, ReactElement } from 'react'; import React, { FunctionComponent, ReactElement } from "react";
export interface ComponentProps { export interface ComponentProps {
onClick: () => void; onClick: () => void;
} }
const Logo: FunctionComponent<ComponentProps> = ( const Logo: FunctionComponent<ComponentProps> = (
props: ComponentProps props: ComponentProps,
): ReactElement => { ): ReactElement => {
return ( return (
<div className="relative z-10 flex px-2 lg:px-0"> <div className="relative z-10 flex px-2 lg:px-0">
<div className="flex flex-shrink-0 items-center"> <div className="flex flex-shrink-0 items-center">
<Image <Image
className="block h-8 w-auto" className="block h-8 w-auto"
onClick={() => { onClick={() => {
props.onClick && props.onClick(); props.onClick && props.onClick();
}} }}
imageUrl={Route.fromString(`${OneUptimeLogo}`)} imageUrl={Route.fromString(`${OneUptimeLogo}`)}
alt={'OneUptime'} alt={"OneUptime"}
/> />
</div> </div>
</div> </div>
); );
}; };
export default Logo; export default Logo;

View File

@@ -1,35 +1,35 @@
import IconProp from 'Common/Types/Icon/IconProp'; import IconProp from "Common/Types/Icon/IconProp";
import HeaderIconDropdownButton from 'CommonUI/src/Components/Header/HeaderIconDropdownButton'; import HeaderIconDropdownButton from "CommonUI/src/Components/Header/HeaderIconDropdownButton";
import NotificationItem from 'CommonUI/src/Components/Header/Notifications/NotificationItem'; import NotificationItem from "CommonUI/src/Components/Header/Notifications/NotificationItem";
import Notifications from 'CommonUI/src/Components/Header/Notifications/Notifications'; import Notifications from "CommonUI/src/Components/Header/Notifications/Notifications";
import React, { ReactElement, useState } from 'react'; import React, { ReactElement, useState } from "react";
const DashboardHeader: () => JSX.Element = (): ReactElement => { const DashboardHeader: () => JSX.Element = (): ReactElement => {
const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false); const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
return ( return (
<HeaderIconDropdownButton <HeaderIconDropdownButton
name="Notifications" name="Notifications"
onClick={() => { onClick={() => {
setIsDropdownVisible(true); setIsDropdownVisible(true);
}} }}
showDropdown={isDropdownVisible} showDropdown={isDropdownVisible}
icon={IconProp.Notification} icon={IconProp.Notification}
badge={4} badge={4}
> >
<Notifications> <Notifications>
<NotificationItem <NotificationItem
title="Sample Title" title="Sample Title"
description="Sample Description" description="Sample Description"
createdAt={new Date()} createdAt={new Date()}
icon={IconProp.Home} icon={IconProp.Home}
onClick={() => { onClick={() => {
setIsDropdownVisible(false); setIsDropdownVisible(false);
}} }}
/> />
</Notifications> </Notifications>
</HeaderIconDropdownButton> </HeaderIconDropdownButton>
); );
}; };
export default DashboardHeader; export default DashboardHeader;

View File

@@ -1,286 +1,273 @@
import SubscriptionPlan from 'Common/Types/Billing/SubscriptionPlan'; import SubscriptionPlan from "Common/Types/Billing/SubscriptionPlan";
import IconProp from 'Common/Types/Icon/IconProp'; import IconProp from "Common/Types/Icon/IconProp";
import { FormType } from 'CommonUI/src/Components/Forms/ModelForm'; import { FormType } from "CommonUI/src/Components/Forms/ModelForm";
import Field from 'CommonUI/src/Components/Forms/Types/Field'; import Field from "CommonUI/src/Components/Forms/Types/Field";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import ProjectPicker from 'CommonUI/src/Components/Header/ProjectPicker/ProjectPicker'; import ProjectPicker from "CommonUI/src/Components/Header/ProjectPicker/ProjectPicker";
import ModelFormModal from 'CommonUI/src/Components/ModelFormModal/ModelFormModal'; import ModelFormModal from "CommonUI/src/Components/ModelFormModal/ModelFormModal";
import { RadioButton } from 'CommonUI/src/Components/RadioButtons/GroupRadioButtons'; import { RadioButton } from "CommonUI/src/Components/RadioButtons/GroupRadioButtons";
import Toggle from 'CommonUI/src/Components/Toggle/Toggle'; import Toggle from "CommonUI/src/Components/Toggle/Toggle";
import { BILLING_ENABLED, getAllEnvVars } from 'CommonUI/src/Config'; import { BILLING_ENABLED, getAllEnvVars } from "CommonUI/src/Config";
import { GetReactElementFunction } from 'CommonUI/src/Types/FunctionTypes'; import { GetReactElementFunction } from "CommonUI/src/Types/FunctionTypes";
import ProjectUtil from 'CommonUI/src/Utils/Project'; import ProjectUtil from "CommonUI/src/Utils/Project";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
import React, { import React, {
FunctionComponent, FunctionComponent,
ReactElement, ReactElement,
useEffect, useEffect,
useState, useState,
} from 'react'; } from "react";
export interface ComponentProps { export interface ComponentProps {
projects: Array<Project>; projects: Array<Project>;
onProjectSelected: (project: Project) => void; onProjectSelected: (project: Project) => void;
showProjectModal: boolean; showProjectModal: boolean;
onProjectModalClose: () => void; onProjectModalClose: () => void;
} }
const DashboardProjectPicker: FunctionComponent<ComponentProps> = ( const DashboardProjectPicker: FunctionComponent<ComponentProps> = (
props: ComponentProps props: ComponentProps,
): ReactElement => { ): ReactElement => {
const [showModal, setShowModal] = useState<boolean>(false); const [showModal, setShowModal] = useState<boolean>(false);
const [selectedProject, setSelectedProject] = useState<Project | null>( const [selectedProject, setSelectedProject] = useState<Project | null>(null);
null
);
const getFooter: GetReactElementFunction = (): ReactElement => { const getFooter: GetReactElementFunction = (): ReactElement => {
if (!BILLING_ENABLED) { if (!BILLING_ENABLED) {
return <></>; return <></>;
} }
return (
<Toggle
title="Yearly Plan"
value={isSubscriptionPlanYearly}
description="(Save 20%)"
onChange={(value: boolean) => {
setIsSubscriptionPlanYearly(value);
}}
/>
);
};
const [isSubscriptionPlanYearly, setIsSubscriptionPlanYearly] =
useState<boolean>(true);
const [fields, setFields] = useState<Array<Field<Project>>>([]);
useEffect(() => {
if (props.showProjectModal) {
setShowModal(true);
}
}, [props.showProjectModal]);
useEffect(() => {
const currentProject: Project | null = ProjectUtil.getCurrentProject();
setSelectedProject(currentProject);
if (currentProject && props.onProjectSelected) {
props.onProjectSelected(currentProject);
}
}, []);
useEffect(() => {
if (selectedProject) {
ProjectUtil.setCurrentProject(selectedProject);
if (props.onProjectSelected) {
props.onProjectSelected(selectedProject);
}
}
}, [selectedProject]);
useEffect(() => {
if (
props.projects &&
props.projects.length > 0 &&
!selectedProject &&
props.projects[0]
) {
const currentProject: Project | null =
ProjectUtil.getCurrentProject();
if (!currentProject) {
setSelectedProject(props.projects[0]);
} else if (
props.projects.filter((project: Project) => {
return project._id === currentProject._id;
}).length > 0
) {
setSelectedProject(
props.projects.filter((project: Project) => {
return project._id === currentProject._id;
})[0] as Project
);
} else {
setSelectedProject(props.projects[0]);
}
}
}, [props.projects]);
useEffect(() => {
refreshFields();
}, [isSubscriptionPlanYearly]);
const refreshFields: VoidFunction = (): void => {
let formFields: Array<Field<Project>> = [
{
field: {
name: true,
},
validation: {
minLength: 4,
},
fieldType: FormFieldSchemaType.Text,
placeholder: 'My Project',
description: 'Pick a friendly name.',
title: 'Project Name',
required: true,
stepId: BILLING_ENABLED ? 'basic' : undefined,
},
];
if (BILLING_ENABLED) {
formFields = [
...formFields,
{
field: {
paymentProviderPlanId: true,
},
stepId: 'plan',
validation: {
minLength: 6,
},
footerElement: getFooter(),
fieldType: FormFieldSchemaType.RadioButton,
radioButtonOptions: SubscriptionPlan.getSubscriptionPlans(
getAllEnvVars()
).map((plan: SubscriptionPlan): RadioButton => {
let description: string = plan.isCustomPricing()
? `Our sales team will contact you soon.`
: `Billed ${
isSubscriptionPlanYearly
? 'yearly'
: 'monthly'
}. ${
plan.getTrialPeriod() > 0
? `Free ${plan.getTrialPeriod()} days trial.`
: ''
}`;
if (
isSubscriptionPlanYearly &&
plan.getYearlySubscriptionAmountInUSD() === 0
) {
description = 'This plan is free, forever. ';
}
if (
!isSubscriptionPlanYearly &&
plan.getMonthlySubscriptionAmountInUSD() === 0
) {
description = 'This plan is free, forever. ';
}
return {
value: isSubscriptionPlanYearly
? plan.getYearlyPlanId()
: plan.getMonthlyPlanId(),
title: plan.getName(),
description: description,
sideTitle: plan.isCustomPricing()
? 'Custom Price'
: isSubscriptionPlanYearly
? '$' +
plan
.getYearlySubscriptionAmountInUSD()
.toString() +
'/mo billed yearly'
: '$' +
plan
.getMonthlySubscriptionAmountInUSD()
.toString(),
sideDescription: plan.isCustomPricing()
? ''
: isSubscriptionPlanYearly
? `~ $${
plan.getYearlySubscriptionAmountInUSD() *
12
} per user / year`
: `/month per user`,
};
}),
title: 'Please select a plan.',
required: true,
},
{
field: {
paymentProviderPromoCode: true,
},
fieldType: FormFieldSchemaType.Text,
placeholder: 'Promo Code (Optional)',
description: 'If you have a coupon code, enter it here.',
title: 'Promo Code',
required: false,
stepId: 'plan',
},
];
}
setFields(formFields);
};
return ( return (
<> <Toggle
{props.projects.length !== 0 && ( title="Yearly Plan"
<ProjectPicker value={isSubscriptionPlanYearly}
selectedProjectName={selectedProject?.name || ''} description="(Save 20%)"
selectedProjectIcon={IconProp.Folder} onChange={(value: boolean) => {
projects={props.projects} setIsSubscriptionPlanYearly(value);
onCreateProjectButtonClicked={() => { }}
setShowModal(true); />
props.onProjectModalClose();
}}
onProjectSelected={(project: Project) => {
setSelectedProject(project);
}}
/>
)}
{showModal ? (
<ModelFormModal<Project>
modelType={Project}
name="Create New Project"
title="Create New Project"
description="Please create a new OneUptime project to get started."
onClose={() => {
setShowModal(false);
props.onProjectModalClose();
}}
submitButtonText="Create Project"
onSuccess={(project: Project | null) => {
setSelectedProject(project);
if (project && props.onProjectSelected) {
props.onProjectSelected(project);
}
setShowModal(false);
props.onProjectModalClose();
}}
formProps={{
name: 'Create New Project',
steps: BILLING_ENABLED
? [
{
title: 'Basic',
id: 'basic',
},
{
title: 'Select Plan',
id: 'plan',
},
]
: undefined,
saveRequestOptions: {
isMultiTenantRequest: true, // because this is a tenant request, we do not have to include the header in the request
},
modelType: Project,
id: 'create-project-from',
fields: [...fields],
formType: FormType.Create,
}}
/>
) : (
<></>
)}
</>
); );
};
const [isSubscriptionPlanYearly, setIsSubscriptionPlanYearly] =
useState<boolean>(true);
const [fields, setFields] = useState<Array<Field<Project>>>([]);
useEffect(() => {
if (props.showProjectModal) {
setShowModal(true);
}
}, [props.showProjectModal]);
useEffect(() => {
const currentProject: Project | null = ProjectUtil.getCurrentProject();
setSelectedProject(currentProject);
if (currentProject && props.onProjectSelected) {
props.onProjectSelected(currentProject);
}
}, []);
useEffect(() => {
if (selectedProject) {
ProjectUtil.setCurrentProject(selectedProject);
if (props.onProjectSelected) {
props.onProjectSelected(selectedProject);
}
}
}, [selectedProject]);
useEffect(() => {
if (
props.projects &&
props.projects.length > 0 &&
!selectedProject &&
props.projects[0]
) {
const currentProject: Project | null = ProjectUtil.getCurrentProject();
if (!currentProject) {
setSelectedProject(props.projects[0]);
} else if (
props.projects.filter((project: Project) => {
return project._id === currentProject._id;
}).length > 0
) {
setSelectedProject(
props.projects.filter((project: Project) => {
return project._id === currentProject._id;
})[0] as Project,
);
} else {
setSelectedProject(props.projects[0]);
}
}
}, [props.projects]);
useEffect(() => {
refreshFields();
}, [isSubscriptionPlanYearly]);
const refreshFields: VoidFunction = (): void => {
let formFields: Array<Field<Project>> = [
{
field: {
name: true,
},
validation: {
minLength: 4,
},
fieldType: FormFieldSchemaType.Text,
placeholder: "My Project",
description: "Pick a friendly name.",
title: "Project Name",
required: true,
stepId: BILLING_ENABLED ? "basic" : undefined,
},
];
if (BILLING_ENABLED) {
formFields = [
...formFields,
{
field: {
paymentProviderPlanId: true,
},
stepId: "plan",
validation: {
minLength: 6,
},
footerElement: getFooter(),
fieldType: FormFieldSchemaType.RadioButton,
radioButtonOptions: SubscriptionPlan.getSubscriptionPlans(
getAllEnvVars(),
).map((plan: SubscriptionPlan): RadioButton => {
let description: string = plan.isCustomPricing()
? `Our sales team will contact you soon.`
: `Billed ${isSubscriptionPlanYearly ? "yearly" : "monthly"}. ${
plan.getTrialPeriod() > 0
? `Free ${plan.getTrialPeriod()} days trial.`
: ""
}`;
if (
isSubscriptionPlanYearly &&
plan.getYearlySubscriptionAmountInUSD() === 0
) {
description = "This plan is free, forever. ";
}
if (
!isSubscriptionPlanYearly &&
plan.getMonthlySubscriptionAmountInUSD() === 0
) {
description = "This plan is free, forever. ";
}
return {
value: isSubscriptionPlanYearly
? plan.getYearlyPlanId()
: plan.getMonthlyPlanId(),
title: plan.getName(),
description: description,
sideTitle: plan.isCustomPricing()
? "Custom Price"
: isSubscriptionPlanYearly
? "$" +
plan.getYearlySubscriptionAmountInUSD().toString() +
"/mo billed yearly"
: "$" + plan.getMonthlySubscriptionAmountInUSD().toString(),
sideDescription: plan.isCustomPricing()
? ""
: isSubscriptionPlanYearly
? `~ $${
plan.getYearlySubscriptionAmountInUSD() * 12
} per user / year`
: `/month per user`,
};
}),
title: "Please select a plan.",
required: true,
},
{
field: {
paymentProviderPromoCode: true,
},
fieldType: FormFieldSchemaType.Text,
placeholder: "Promo Code (Optional)",
description: "If you have a coupon code, enter it here.",
title: "Promo Code",
required: false,
stepId: "plan",
},
];
}
setFields(formFields);
};
return (
<>
{props.projects.length !== 0 && (
<ProjectPicker
selectedProjectName={selectedProject?.name || ""}
selectedProjectIcon={IconProp.Folder}
projects={props.projects}
onCreateProjectButtonClicked={() => {
setShowModal(true);
props.onProjectModalClose();
}}
onProjectSelected={(project: Project) => {
setSelectedProject(project);
}}
/>
)}
{showModal ? (
<ModelFormModal<Project>
modelType={Project}
name="Create New Project"
title="Create New Project"
description="Please create a new OneUptime project to get started."
onClose={() => {
setShowModal(false);
props.onProjectModalClose();
}}
submitButtonText="Create Project"
onSuccess={(project: Project | null) => {
setSelectedProject(project);
if (project && props.onProjectSelected) {
props.onProjectSelected(project);
}
setShowModal(false);
props.onProjectModalClose();
}}
formProps={{
name: "Create New Project",
steps: BILLING_ENABLED
? [
{
title: "Basic",
id: "basic",
},
{
title: "Select Plan",
id: "plan",
},
]
: undefined,
saveRequestOptions: {
isMultiTenantRequest: true, // because this is a tenant request, we do not have to include the header in the request
},
modelType: Project,
id: "create-project-from",
fields: [...fields],
formType: FormType.Create,
}}
/>
) : (
<></>
)}
</>
);
}; };
export default DashboardProjectPicker; export default DashboardProjectPicker;

View File

@@ -1,20 +1,20 @@
import SearchBox from 'CommonUI/src/Components/Header/SearchBox'; import SearchBox from "CommonUI/src/Components/Header/SearchBox";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
import React, { FunctionComponent, ReactElement } from 'react'; import React, { FunctionComponent, ReactElement } from "react";
export interface ComponentProps { export interface ComponentProps {
onChange: (search: string) => void; onChange: (search: string) => void;
selectedProject: Project | null; selectedProject: Project | null;
} }
const Search: FunctionComponent<ComponentProps> = ( const Search: FunctionComponent<ComponentProps> = (
props: ComponentProps props: ComponentProps,
): ReactElement => { ): ReactElement => {
if (!props.selectedProject) { if (!props.selectedProject) {
return <></>; return <></>;
} }
return <SearchBox key={2} onChange={props.onChange} />; return <SearchBox key={2} onChange={props.onChange} />;
}; };
export default Search; export default Search;

View File

@@ -1,57 +1,57 @@
import PageMap from '../../Utils/PageMap'; import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import IconProp from 'Common/Types/Icon/IconProp'; import IconProp from "Common/Types/Icon/IconProp";
import HeaderIconDropdownButton from 'CommonUI/src/Components/Header/HeaderIconDropdownButton'; import HeaderIconDropdownButton from "CommonUI/src/Components/Header/HeaderIconDropdownButton";
import IconDropdownItem from 'CommonUI/src/Components/Header/IconDropdown/IconDropdownItem'; import IconDropdownItem from "CommonUI/src/Components/Header/IconDropdown/IconDropdownItem";
import IconDropdownMenu from 'CommonUI/src/Components/Header/IconDropdown/IconDropdownMenu'; import IconDropdownMenu from "CommonUI/src/Components/Header/IconDropdown/IconDropdownMenu";
import { DASHBOARD_URL } from 'CommonUI/src/Config'; import { DASHBOARD_URL } from "CommonUI/src/Config";
import BlankProfilePic from 'CommonUI/src/Images/users/blank-profile.svg'; import BlankProfilePic from "CommonUI/src/Images/users/blank-profile.svg";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import User from 'CommonUI/src/Utils/User'; import User from "CommonUI/src/Utils/User";
import React, { FunctionComponent, ReactElement, useState } from 'react'; import React, { FunctionComponent, ReactElement, useState } from "react";
const DashboardUserProfile: FunctionComponent = (): ReactElement => { const DashboardUserProfile: FunctionComponent = (): ReactElement => {
const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false); const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
return ( return (
<> <>
<HeaderIconDropdownButton <HeaderIconDropdownButton
iconImageUrl={BlankProfilePic} iconImageUrl={BlankProfilePic}
name="User Profile" name="User Profile"
showDropdown={isDropdownVisible} showDropdown={isDropdownVisible}
onClick={() => { onClick={() => {
setIsDropdownVisible(true); setIsDropdownVisible(true);
}} }}
> >
<IconDropdownMenu> <IconDropdownMenu>
{User.isMasterAdmin() ? ( {User.isMasterAdmin() ? (
<IconDropdownItem <IconDropdownItem
title="Exit Admin" title="Exit Admin"
onClick={() => { onClick={() => {
setIsDropdownVisible(false); setIsDropdownVisible(false);
Navigation.navigate(DASHBOARD_URL); Navigation.navigate(DASHBOARD_URL);
}} }}
icon={IconProp.ExternalLink} icon={IconProp.ExternalLink}
/> />
) : ( ) : (
<></> <></>
)} )}
<IconDropdownItem <IconDropdownItem
title="Log out" title="Log out"
onClick={() => { onClick={() => {
setIsDropdownVisible(false); setIsDropdownVisible(false);
}} }}
url={RouteUtil.populateRouteParams( url={RouteUtil.populateRouteParams(
RouteMap[PageMap.LOGOUT] as Route RouteMap[PageMap.LOGOUT] as Route,
)} )}
icon={IconProp.Logout} icon={IconProp.Logout}
/> />
</IconDropdownMenu> </IconDropdownMenu>
</HeaderIconDropdownButton> </HeaderIconDropdownButton>
</> </>
); );
}; };
export default DashboardUserProfile; export default DashboardUserProfile;

View File

@@ -1,35 +1,35 @@
import Footer from '../Footer/Footer'; import Footer from "../Footer/Footer";
import Header from '../Header/Header'; import Header from "../Header/Header";
import NavBar from '../NavBar/NavBar'; import NavBar from "../NavBar/NavBar";
import MasterPage from 'CommonUI/src/Components/MasterPage/MasterPage'; import MasterPage from "CommonUI/src/Components/MasterPage/MasterPage";
import TopAlert from 'CommonUI/src/Components/TopAlert/TopAlert'; import TopAlert from "CommonUI/src/Components/TopAlert/TopAlert";
import React, { FunctionComponent, ReactElement } from 'react'; import React, { FunctionComponent, ReactElement } from "react";
export interface ComponentProps { export interface ComponentProps {
children: ReactElement | Array<ReactElement>; children: ReactElement | Array<ReactElement>;
} }
const DashboardMasterPage: FunctionComponent<ComponentProps> = ( const DashboardMasterPage: FunctionComponent<ComponentProps> = (
props: ComponentProps props: ComponentProps,
): ReactElement => { ): ReactElement => {
return ( return (
<div> <div>
<TopAlert <TopAlert
title="OneUptime Admin Dashboard" title="OneUptime Admin Dashboard"
description="You can perform your OneUptime server related tasks on this dashboard." description="You can perform your OneUptime server related tasks on this dashboard."
/> />
<MasterPage <MasterPage
footer={<Footer />} footer={<Footer />}
header={<Header />} header={<Header />}
navBar={<NavBar />} navBar={<NavBar />}
isLoading={false} isLoading={false}
error={''} error={""}
className="flex flex-col h-screen justify-between" className="flex flex-col h-screen justify-between"
> >
{props.children} {props.children}
</MasterPage> </MasterPage>
</div> </div>
); );
}; };
export default DashboardMasterPage; export default DashboardMasterPage;

View File

@@ -1,39 +1,37 @@
import PageMap from '../../Utils/PageMap'; import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import IconProp from 'Common/Types/Icon/IconProp'; import IconProp from "Common/Types/Icon/IconProp";
import NavBar from 'CommonUI/src/Components/Navbar/NavBar'; import NavBar from "CommonUI/src/Components/Navbar/NavBar";
import NavBarItem from 'CommonUI/src/Components/Navbar/NavBarItem'; import NavBarItem from "CommonUI/src/Components/Navbar/NavBarItem";
import React, { FunctionComponent, ReactElement } from 'react'; import React, { FunctionComponent, ReactElement } from "react";
const DashboardNavbar: FunctionComponent = (): ReactElement => { const DashboardNavbar: FunctionComponent = (): ReactElement => {
return ( return (
<NavBar> <NavBar>
<NavBarItem <NavBarItem
title="Users" title="Users"
icon={IconProp.User} icon={IconProp.User}
route={RouteUtil.populateRouteParams( route={RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route)}
RouteMap[PageMap.USERS] as Route ></NavBarItem>
)}
></NavBarItem>
<NavBarItem <NavBarItem
title="Projects" title="Projects"
icon={IconProp.Folder} icon={IconProp.Folder}
route={RouteUtil.populateRouteParams( route={RouteUtil.populateRouteParams(
RouteMap[PageMap.PROJECTS] as Route RouteMap[PageMap.PROJECTS] as Route,
)} )}
></NavBarItem> ></NavBarItem>
<NavBarItem <NavBarItem
title="Settings" title="Settings"
icon={IconProp.Settings} icon={IconProp.Settings}
route={RouteUtil.populateRouteParams( route={RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS] as Route RouteMap[PageMap.SETTINGS] as Route,
)} )}
></NavBarItem> ></NavBarItem>
</NavBar> </NavBar>
); );
}; };
export default DashboardNavbar; export default DashboardNavbar;

View File

@@ -1,19 +1,19 @@
import App from './App'; import App from "./App";
import Telemetry from 'CommonUI/src/Utils/Telemetry'; import Telemetry from "CommonUI/src/Utils/Telemetry";
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom/client'; import ReactDOM from "react-dom/client";
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from "react-router-dom";
Telemetry.init({ Telemetry.init({
serviceName: 'AdminDashboard', serviceName: "AdminDashboard",
}); });
const root: any = ReactDOM.createRoot( const root: any = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById("root") as HTMLElement,
); );
root.render( root.render(
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>,
); );

View File

@@ -1,22 +1,22 @@
import PageMap from '../../Utils/PageMap'; import PageMap from "../../Utils/PageMap";
import RouteMap from '../../Utils/RouteMap'; import RouteMap from "../../Utils/RouteMap";
import PageLoader from 'CommonUI/src/Components/Loader/PageLoader'; import PageLoader from "CommonUI/src/Components/Loader/PageLoader";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import React, { FunctionComponent, ReactElement, useEffect } from 'react'; import React, { FunctionComponent, ReactElement, useEffect } from "react";
const Init: FunctionComponent = (): ReactElement => { const Init: FunctionComponent = (): ReactElement => {
useEffect(() => { useEffect(() => {
Navigation.navigate(RouteMap[PageMap.USERS]!, { Navigation.navigate(RouteMap[PageMap.USERS]!, {
forceNavigate: true, forceNavigate: true,
}); });
}, []); }, []);
return ( return (
<Page title={''} breadcrumbLinks={[]}> <Page title={""} breadcrumbLinks={[]}>
<PageLoader isVisible={true} /> <PageLoader isVisible={true} />
</Page> </Page>
); );
}; };
export default Init; export default Init;

View File

@@ -1,53 +1,49 @@
import PageMap from '../../Utils/PageMap'; import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes'; import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage'; import ErrorMessage from "CommonUI/src/Components/ErrorMessage/ErrorMessage";
import PageLoader from 'CommonUI/src/Components/Loader/PageLoader'; import PageLoader from "CommonUI/src/Components/Loader/PageLoader";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import { ACCOUNTS_URL } from 'CommonUI/src/Config'; import { ACCOUNTS_URL } from "CommonUI/src/Config";
import UiAnalytics from 'CommonUI/src/Utils/Analytics'; import UiAnalytics from "CommonUI/src/Utils/Analytics";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import UserUtil from 'CommonUI/src/Utils/User'; import UserUtil from "CommonUI/src/Utils/User";
import React, { FunctionComponent, ReactElement, useEffect } from 'react'; import React, { FunctionComponent, ReactElement, useEffect } from "react";
const Logout: FunctionComponent = (): ReactElement => { const Logout: FunctionComponent = (): ReactElement => {
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
const logout: PromiseVoidFunction = async (): Promise<void> => { const logout: PromiseVoidFunction = async (): Promise<void> => {
UiAnalytics.logout(); UiAnalytics.logout();
await UserUtil.logout(); await UserUtil.logout();
Navigation.navigate(ACCOUNTS_URL); Navigation.navigate(ACCOUNTS_URL);
}; };
useEffect(() => { useEffect(() => {
logout().catch((error: Error) => { logout().catch((error: Error) => {
setError(error.message || error.toString()); setError(error.message || error.toString());
}); });
}, []); }, []);
return ( return (
<Page <Page
title={'Logout'} title={"Logout"}
breadcrumbLinks={[ breadcrumbLinks={[
{ {
title: 'Admin Dashboard', title: "Admin Dashboard",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(RouteMap[PageMap.INIT] as Route),
RouteMap[PageMap.INIT] as Route },
), {
}, title: "Logout",
{ to: RouteUtil.populateRouteParams(RouteMap[PageMap.LOGOUT] as Route),
title: 'Logout', },
to: RouteUtil.populateRouteParams( ]}
RouteMap[PageMap.LOGOUT] as Route >
), {!error ? <PageLoader isVisible={true} /> : <></>}
}, {error ? <ErrorMessage error={error} /> : <></>}
]} </Page>
> );
{!error ? <PageLoader isVisible={true} /> : <></>}
{error ? <ErrorMessage error={error} /> : <></>}
</Page>
);
}; };
export default Logout; export default Logout;

View File

@@ -1,263 +1,251 @@
import AdminModelAPI from '../../Utils/ModelAPI'; import AdminModelAPI from "../../Utils/ModelAPI";
import PageMap from '../../Utils/PageMap'; import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import SubscriptionPlan from 'Common/Types/Billing/SubscriptionPlan'; import SubscriptionPlan from "Common/Types/Billing/SubscriptionPlan";
import Field from 'CommonUI/src/Components/Forms/Types/Field'; import Field from "CommonUI/src/Components/Forms/Types/Field";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable'; import ModelTable from "CommonUI/src/Components/ModelTable/ModelTable";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import { RadioButton } from 'CommonUI/src/Components/RadioButtons/GroupRadioButtons'; import { RadioButton } from "CommonUI/src/Components/RadioButtons/GroupRadioButtons";
import Toggle from 'CommonUI/src/Components/Toggle/Toggle'; import Toggle from "CommonUI/src/Components/Toggle/Toggle";
import FieldType from 'CommonUI/src/Components/Types/FieldType'; import FieldType from "CommonUI/src/Components/Types/FieldType";
import { BILLING_ENABLED, getAllEnvVars } from 'CommonUI/src/Config'; import { BILLING_ENABLED, getAllEnvVars } from "CommonUI/src/Config";
import { GetReactElementFunction } from 'CommonUI/src/Types/FunctionTypes'; import { GetReactElementFunction } from "CommonUI/src/Types/FunctionTypes";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
import User from 'Model/Models/User'; import User from "Model/Models/User";
import React, { import React, {
FunctionComponent, FunctionComponent,
ReactElement, ReactElement,
useEffect, useEffect,
useState, useState,
} from 'react'; } from "react";
const Projects: FunctionComponent = (): ReactElement => { const Projects: FunctionComponent = (): ReactElement => {
const [isSubscriptionPlanYearly, setIsSubscriptionPlanYearly] = const [isSubscriptionPlanYearly, setIsSubscriptionPlanYearly] =
useState<boolean>(true); useState<boolean>(true);
useEffect(() => { useEffect(() => {
refreshFields(); refreshFields();
}, [isSubscriptionPlanYearly]); }, [isSubscriptionPlanYearly]);
const refreshFields: VoidFunction = (): void => { const refreshFields: VoidFunction = (): void => {
let formFields: Array<Field<Project>> = [ let formFields: Array<Field<Project>> = [
{ {
field: { field: {
name: true, name: true,
}, },
validation: { validation: {
minLength: 4, minLength: 4,
}, },
fieldType: FormFieldSchemaType.Text, fieldType: FormFieldSchemaType.Text,
placeholder: 'My Project', placeholder: "My Project",
description: 'Pick a friendly name.', description: "Pick a friendly name.",
title: 'Project Name', title: "Project Name",
required: true, required: true,
stepId: BILLING_ENABLED ? 'basic' : undefined, stepId: BILLING_ENABLED ? "basic" : undefined,
}, },
{ {
field: { field: {
createdByUser: true, createdByUser: true,
}, },
title: 'Owner', title: "Owner",
description: description:
'Who would you like the owner of this project to be? If you leave this blank - you will be the owner of the project', "Who would you like the owner of this project to be? If you leave this blank - you will be the owner of the project",
fieldType: FormFieldSchemaType.Dropdown, fieldType: FormFieldSchemaType.Dropdown,
stepId: BILLING_ENABLED ? 'basic' : undefined, stepId: BILLING_ENABLED ? "basic" : undefined,
dropdownModal: { dropdownModal: {
type: User, type: User,
labelField: 'email', labelField: "email",
valueField: '_id', valueField: "_id",
}, },
}, },
]; ];
if (BILLING_ENABLED) { if (BILLING_ENABLED) {
formFields = [ formFields = [
...formFields, ...formFields,
{ {
field: { field: {
paymentProviderPlanId: true, paymentProviderPlanId: true,
}, },
stepId: 'plan', stepId: "plan",
validation: { validation: {
minLength: 6, minLength: 6,
}, },
footerElement: getFooter(), footerElement: getFooter(),
fieldType: FormFieldSchemaType.RadioButton, fieldType: FormFieldSchemaType.RadioButton,
radioButtonOptions: SubscriptionPlan.getSubscriptionPlans( radioButtonOptions: SubscriptionPlan.getSubscriptionPlans(
getAllEnvVars() getAllEnvVars(),
).map((plan: SubscriptionPlan): RadioButton => { ).map((plan: SubscriptionPlan): RadioButton => {
let description: string = plan.isCustomPricing() let description: string = plan.isCustomPricing()
? `Our sales team will contact you soon.` ? `Our sales team will contact you soon.`
: `Billed ${ : `Billed ${isSubscriptionPlanYearly ? "yearly" : "monthly"}. ${
isSubscriptionPlanYearly plan.getTrialPeriod() > 0
? 'yearly' ? `Free ${plan.getTrialPeriod()} days trial.`
: 'monthly' : ""
}. ${ }`;
plan.getTrialPeriod() > 0
? `Free ${plan.getTrialPeriod()} days trial.`
: ''
}`;
if ( if (
isSubscriptionPlanYearly && isSubscriptionPlanYearly &&
plan.getYearlySubscriptionAmountInUSD() === 0 plan.getYearlySubscriptionAmountInUSD() === 0
) { ) {
description = 'This plan is free, forever. '; description = "This plan is free, forever. ";
} }
if ( if (
!isSubscriptionPlanYearly && !isSubscriptionPlanYearly &&
plan.getMonthlySubscriptionAmountInUSD() === 0 plan.getMonthlySubscriptionAmountInUSD() === 0
) { ) {
description = 'This plan is free, forever. '; description = "This plan is free, forever. ";
} }
return { return {
value: isSubscriptionPlanYearly value: isSubscriptionPlanYearly
? plan.getYearlyPlanId() ? plan.getYearlyPlanId()
: plan.getMonthlyPlanId(), : plan.getMonthlyPlanId(),
title: plan.getName(), title: plan.getName(),
description: description, description: description,
sideTitle: plan.isCustomPricing() sideTitle: plan.isCustomPricing()
? 'Custom Price' ? "Custom Price"
: isSubscriptionPlanYearly : isSubscriptionPlanYearly
? '$' + ? "$" +
plan plan.getYearlySubscriptionAmountInUSD().toString() +
.getYearlySubscriptionAmountInUSD() "/mo billed yearly"
.toString() + : "$" + plan.getMonthlySubscriptionAmountInUSD().toString(),
'/mo billed yearly' sideDescription: plan.isCustomPricing()
: '$' + ? ""
plan : isSubscriptionPlanYearly
.getMonthlySubscriptionAmountInUSD() ? `~ $${
.toString(), plan.getYearlySubscriptionAmountInUSD() * 12
sideDescription: plan.isCustomPricing() } per user / year`
? '' : `/month per user`,
: isSubscriptionPlanYearly };
? `~ $${ }),
plan.getYearlySubscriptionAmountInUSD() * title: "Please select a plan.",
12 required: true,
} per user / year` },
: `/month per user`, {
}; field: {
}), paymentProviderPromoCode: true,
title: 'Please select a plan.', },
required: true, fieldType: FormFieldSchemaType.Text,
}, placeholder: "Promo Code (Optional)",
{ description: "If you have a coupon code, enter it here.",
field: { title: "Promo Code",
paymentProviderPromoCode: true, required: false,
}, stepId: "plan",
fieldType: FormFieldSchemaType.Text, disabled: false,
placeholder: 'Promo Code (Optional)', },
description: 'If you have a coupon code, enter it here.', ];
title: 'Promo Code', }
required: false,
stepId: 'plan',
disabled: false,
},
];
}
setFields(formFields); setFields(formFields);
}; };
const [fields, setFields] = useState<Array<Field<Project>>>([]); const [fields, setFields] = useState<Array<Field<Project>>>([]);
const getFooter: GetReactElementFunction = (): ReactElement => { const getFooter: GetReactElementFunction = (): ReactElement => {
if (!BILLING_ENABLED) { if (!BILLING_ENABLED) {
return <></>; return <></>;
} }
return (
<Toggle
title="Yearly Plan"
value={isSubscriptionPlanYearly}
description="(Save 20%)"
onChange={(value: boolean) => {
setIsSubscriptionPlanYearly(value);
}}
/>
);
};
return ( return (
<Page <Toggle
title={'Projects'} title="Yearly Plan"
breadcrumbLinks={[ value={isSubscriptionPlanYearly}
{ description="(Save 20%)"
title: 'Admin Dashboard', onChange={(value: boolean) => {
to: RouteUtil.populateRouteParams( setIsSubscriptionPlanYearly(value);
RouteMap[PageMap.HOME] as Route }}
), />
},
{
title: 'Projects',
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.PROJECTS] as Route
),
},
]}
>
<ModelTable<Project>
modelType={Project}
modelAPI={AdminModelAPI}
id="projects-table"
isDeleteable={false}
isEditable={false}
isCreateable={true}
name="Projects"
isViewable={false}
cardProps={{
title: 'Projects',
description: 'Here is a list of proejcts in OneUptime.',
}}
showViewIdButton={true}
formSteps={
BILLING_ENABLED
? [
{
title: 'Basic',
id: 'basic',
},
{
title: 'Select Plan',
id: 'plan',
},
]
: undefined
}
noItemsMessage={'No projects found.'}
formFields={fields}
showRefreshButton={true}
viewPageRoute={Navigation.getCurrentRoute()}
filters={[
{
field: {
name: true,
},
title: 'Name',
type: FieldType.Text,
},
{
field: {
createdAt: true,
},
title: 'Created At',
type: FieldType.DateTime,
},
]}
columns={[
{
field: {
name: true,
},
title: 'Name',
type: FieldType.Text,
},
{
field: {
createdAt: true,
},
title: 'Created At',
type: FieldType.DateTime,
},
]}
/>
</Page>
); );
};
return (
<Page
title={"Projects"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "Projects",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.PROJECTS] as Route,
),
},
]}
>
<ModelTable<Project>
modelType={Project}
modelAPI={AdminModelAPI}
id="projects-table"
isDeleteable={false}
isEditable={false}
isCreateable={true}
name="Projects"
isViewable={false}
cardProps={{
title: "Projects",
description: "Here is a list of proejcts in OneUptime.",
}}
showViewIdButton={true}
formSteps={
BILLING_ENABLED
? [
{
title: "Basic",
id: "basic",
},
{
title: "Select Plan",
id: "plan",
},
]
: undefined
}
noItemsMessage={"No projects found."}
formFields={fields}
showRefreshButton={true}
viewPageRoute={Navigation.getCurrentRoute()}
filters={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
createdAt: true,
},
title: "Created At",
type: FieldType.DateTime,
},
]}
columns={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
createdAt: true,
},
title: "Created At",
type: FieldType.DateTime,
},
]}
/>
</Page>
);
}; };
export default Projects; export default Projects;

View File

@@ -1,102 +1,100 @@
import PageMap from '../../../Utils/PageMap'; import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import DashboardSideMenu from '../SideMenu'; import DashboardSideMenu from "../SideMenu";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail'; import CardModelDetail from "CommonUI/src/Components/ModelDetail/CardModelDetail";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import FieldType from 'CommonUI/src/Components/Types/FieldType'; import FieldType from "CommonUI/src/Components/Types/FieldType";
import GlobalConfig from 'Model/Models/GlobalConfig'; import GlobalConfig from "Model/Models/GlobalConfig";
import React, { FunctionComponent, ReactElement } from 'react'; import React, { FunctionComponent, ReactElement } from "react";
const Settings: FunctionComponent = (): ReactElement => { const Settings: FunctionComponent = (): ReactElement => {
return ( return (
<Page <Page
title={'Admin Settings'} title={"Admin Settings"}
breadcrumbLinks={[ breadcrumbLinks={[
{ {
title: 'Admin Dashboard', title: "Admin Dashboard",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
RouteMap[PageMap.HOME] as Route },
), {
}, title: "Settings",
{ to: RouteUtil.populateRouteParams(
title: 'Settings', RouteMap[PageMap.SETTINGS] as Route,
to: RouteUtil.populateRouteParams( ),
RouteMap[PageMap.SETTINGS] as Route },
), {
}, title: "API Key",
{ to: RouteUtil.populateRouteParams(
title: 'API Key', RouteMap[PageMap.SETTINGS_HOST] as Route,
to: RouteUtil.populateRouteParams( ),
RouteMap[PageMap.SETTINGS_HOST] as Route },
), ]}
}, sideMenu={<DashboardSideMenu />}
]} >
sideMenu={<DashboardSideMenu />} {/* Project Settings View */}
> <CardModelDetail
{/* Project Settings View */} name="API Key Settings"
<CardModelDetail cardProps={{
name="API Key Settings" title: "Master API Key Settings",
cardProps={{ description:
title: 'Master API Key Settings', "This API key has root access to all the resources in all the projects on OneUptime.",
description: }}
'This API key has root access to all the resources in all the projects on OneUptime.', isEditable={true}
}} editButtonText="Edit API Key Settings"
isEditable={true} formFields={[
editButtonText="Edit API Key Settings" {
formFields={[ field: {
{ masterApiKey: true,
field: { },
masterApiKey: true, title: "Master API Key",
}, fieldType: FormFieldSchemaType.ObjectID,
title: 'Master API Key', required: false,
fieldType: FormFieldSchemaType.ObjectID, },
required: false, {
}, field: {
{ isMasterApiKeyEnabled: true,
field: { },
isMasterApiKeyEnabled: true, title: "Enabled",
}, fieldType: FormFieldSchemaType.Toggle,
title: 'Enabled', required: false,
fieldType: FormFieldSchemaType.Toggle, },
required: false, ]}
}, modelDetailProps={{
]} modelType: GlobalConfig,
modelDetailProps={{ id: "model-detail-global-config",
modelType: GlobalConfig, fields: [
id: 'model-detail-global-config', {
fields: [ field: {
{ masterApiKey: true,
field: { },
masterApiKey: true, title: "Master API Key",
}, description:
title: 'Master API Key', "This API key has root access to all the resources in all the projects on OneUptime.",
description: fieldType: FieldType.HiddenText,
'This API key has root access to all the resources in all the projects on OneUptime.', opts: {
fieldType: FieldType.HiddenText, isCopyable: true,
opts: { },
isCopyable: true, placeholder: "API Key not generated yet.",
}, },
placeholder: 'API Key not generated yet.', {
}, field: {
{ isMasterApiKeyEnabled: true,
field: { },
isMasterApiKeyEnabled: true, title: "Enabled",
}, description:
title: 'Enabled', "Enable or disable the master API key. If disabled, all requests using this key will fail.",
description: fieldType: FieldType.Boolean,
'Enable or disable the master API key. If disabled, all requests using this key will fail.', placeholder: "Not Enabled",
fieldType: FieldType.Boolean, },
placeholder: 'Not Enabled', ],
}, modelId: ObjectID.getZeroObjectID(),
], }}
modelId: ObjectID.getZeroObjectID(), />
}} </Page>
/> );
</Page>
);
}; };
export default Settings; export default Settings;

View File

@@ -1,83 +1,80 @@
import PageMap from '../../../Utils/PageMap'; import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import DashboardSideMenu from '../SideMenu'; import DashboardSideMenu from "../SideMenu";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail'; import CardModelDetail from "CommonUI/src/Components/ModelDetail/CardModelDetail";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import FieldType from 'CommonUI/src/Components/Types/FieldType'; import FieldType from "CommonUI/src/Components/Types/FieldType";
import GlobalConfig from 'Model/Models/GlobalConfig'; import GlobalConfig from "Model/Models/GlobalConfig";
import React, { FunctionComponent, ReactElement } from 'react'; import React, { FunctionComponent, ReactElement } from "react";
const Settings: FunctionComponent = (): ReactElement => { const Settings: FunctionComponent = (): ReactElement => {
return ( return (
<Page <Page
title={'Admin Settings'} title={"Admin Settings"}
breadcrumbLinks={[ breadcrumbLinks={[
{ {
title: 'Admin Dashboard', title: "Admin Dashboard",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
RouteMap[PageMap.HOME] as Route },
), {
}, title: "Settings",
{ to: RouteUtil.populateRouteParams(
title: 'Settings', RouteMap[PageMap.SETTINGS] as Route,
to: RouteUtil.populateRouteParams( ),
RouteMap[PageMap.SETTINGS] as Route },
), {
}, title: "Authentication",
{ to: RouteUtil.populateRouteParams(
title: 'Authentication', RouteMap[PageMap.SETTINGS_AUTHENTICATION] as Route,
to: RouteUtil.populateRouteParams( ),
RouteMap[PageMap.SETTINGS_AUTHENTICATION] as Route },
), ]}
}, sideMenu={<DashboardSideMenu />}
]} >
sideMenu={<DashboardSideMenu />} {/* Project Settings View */}
> <CardModelDetail
{/* Project Settings View */} name="Authentication Settings"
<CardModelDetail cardProps={{
name="Authentication Settings" title: "Authentication Settings",
cardProps={{ description:
title: 'Authentication Settings', "Authentication Settings for this OneUptime Server instance.",
description: }}
'Authentication Settings for this OneUptime Server instance.', isEditable={true}
}} editButtonText="Edit Settings"
isEditable={true} formFields={[
editButtonText="Edit Settings" {
formFields={[ field: {
{ disableSignup: true,
field: { },
disableSignup: true, title: "Disable Sign Up",
}, fieldType: FormFieldSchemaType.Toggle,
title: 'Disable Sign Up', required: false,
fieldType: FormFieldSchemaType.Toggle, description: "Should we disable sign up of new users to OneUptime?",
required: false, },
description: ]}
'Should we disable sign up of new users to OneUptime?', modelDetailProps={{
}, modelType: GlobalConfig,
]} id: "model-detail-global-config",
modelDetailProps={{ fields: [
modelType: GlobalConfig, {
id: 'model-detail-global-config', field: {
fields: [ disableSignup: true,
{ },
field: { fieldType: FieldType.Boolean,
disableSignup: true, title: "Disable Sign Up",
}, placeholder: "No",
fieldType: FieldType.Boolean, description:
title: 'Disable Sign Up', "Should we disable sign up of new users to OneUptime?",
placeholder: 'No', },
description: ],
'Should we disable sign up of new users to OneUptime?', modelId: ObjectID.getZeroObjectID(),
}, }}
], />
modelId: ObjectID.getZeroObjectID(), </Page>
}} );
/>
</Page>
);
}; };
export default Settings; export default Settings;

View File

@@ -1,119 +1,114 @@
import PageMap from '../../../Utils/PageMap'; import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import DashboardSideMenu from '../SideMenu'; import DashboardSideMenu from "../SideMenu";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail'; import CardModelDetail from "CommonUI/src/Components/ModelDetail/CardModelDetail";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import FieldType from 'CommonUI/src/Components/Types/FieldType'; import FieldType from "CommonUI/src/Components/Types/FieldType";
import GlobalConfig from 'Model/Models/GlobalConfig'; import GlobalConfig from "Model/Models/GlobalConfig";
import React, { FunctionComponent, ReactElement } from 'react'; import React, { FunctionComponent, ReactElement } from "react";
const Settings: FunctionComponent = (): ReactElement => { const Settings: FunctionComponent = (): ReactElement => {
return ( return (
<Page <Page
title={'Admin Settings'} title={"Admin Settings"}
breadcrumbLinks={[ breadcrumbLinks={[
{ {
title: 'Admin Dashboard', title: "Admin Dashboard",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
RouteMap[PageMap.HOME] as Route },
), {
}, title: "Settings",
{ to: RouteUtil.populateRouteParams(
title: 'Settings', RouteMap[PageMap.SETTINGS] as Route,
to: RouteUtil.populateRouteParams( ),
RouteMap[PageMap.SETTINGS] as Route },
), {
}, title: "Calls and SMS",
{ to: RouteUtil.populateRouteParams(
title: 'Calls and SMS', RouteMap[PageMap.SETTINGS_CALL_AND_SMS] as Route,
to: RouteUtil.populateRouteParams( ),
RouteMap[PageMap.SETTINGS_CALL_AND_SMS] as Route },
), ]}
}, sideMenu={<DashboardSideMenu />}
]} >
sideMenu={<DashboardSideMenu />} {/* Project Settings View */}
> <CardModelDetail
{/* Project Settings View */} name="Call and SMS Settings"
<CardModelDetail cardProps={{
name="Call and SMS Settings" title: "Twilio Config",
cardProps={{ description: "This will be used to make Call and send SMS.",
title: 'Twilio Config', }}
description: 'This will be used to make Call and send SMS.', isEditable={true}
}} editButtonText="Edit Twilio Config"
isEditable={true} formFields={[
editButtonText="Edit Twilio Config" {
formFields={[ field: {
{ twilioAccountSID: true,
field: { },
twilioAccountSID: true, title: "Twilio Account SID",
}, fieldType: FormFieldSchemaType.Text,
title: 'Twilio Account SID', required: true,
fieldType: FormFieldSchemaType.Text, description: "You can find this in your Twilio console.",
required: true, placeholder: "",
description: validation: {
'You can find this in your Twilio console.', minLength: 2,
placeholder: '', },
validation: { },
minLength: 2, {
}, field: {
}, twilioAuthToken: true,
{ },
field: { title: "Twilio Auth Token",
twilioAuthToken: true, fieldType: FormFieldSchemaType.Text,
}, required: true,
title: 'Twilio Auth Token', description: "You can find this in your Twilio console.",
fieldType: FormFieldSchemaType.Text, placeholder: "",
required: true, validation: {
description: minLength: 2,
'You can find this in your Twilio console.', },
placeholder: '', },
validation: { {
minLength: 2, field: {
}, twilioPhoneNumber: true,
}, },
{ title: "Twilio Phone Number",
field: { fieldType: FormFieldSchemaType.Phone,
twilioPhoneNumber: true, required: true,
}, description: "You can find this in your Twilio console.",
title: 'Twilio Phone Number', placeholder: "",
fieldType: FormFieldSchemaType.Phone, validation: {
required: true, minLength: 2,
description: },
'You can find this in your Twilio console.', },
placeholder: '', ]}
validation: { modelDetailProps={{
minLength: 2, modelType: GlobalConfig,
}, id: "model-detail-global-config",
}, fields: [
]} {
modelDetailProps={{ field: {
modelType: GlobalConfig, twilioAccountSID: true,
id: 'model-detail-global-config', },
fields: [ title: "Twilio Account SID",
{ placeholder: "None",
field: { },
twilioAccountSID: true, {
}, field: {
title: 'Twilio Account SID', twilioPhoneNumber: true,
placeholder: 'None', },
}, title: "Twilio Phone Number",
{ fieldType: FieldType.Phone,
field: { placeholder: "None",
twilioPhoneNumber: true, },
}, ],
title: 'Twilio Phone Number', modelId: ObjectID.getZeroObjectID(),
fieldType: FieldType.Phone, }}
placeholder: 'None', />
}, </Page>
], );
modelId: ObjectID.getZeroObjectID(),
}}
/>
</Page>
);
}; };
export default Settings; export default Settings;

View File

@@ -1,427 +1,419 @@
import PageMap from '../../../Utils/PageMap'; import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import DashboardSideMenu from '../SideMenu'; import DashboardSideMenu from "../SideMenu";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import { Green, Red } from 'Common/Types/BrandColors'; import { Green, Red } from "Common/Types/BrandColors";
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes'; import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage'; import ErrorMessage from "CommonUI/src/Components/ErrorMessage/ErrorMessage";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import PageLoader from 'CommonUI/src/Components/Loader/PageLoader'; import PageLoader from "CommonUI/src/Components/Loader/PageLoader";
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail'; import CardModelDetail from "CommonUI/src/Components/ModelDetail/CardModelDetail";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import Pill from 'CommonUI/src/Components/Pill/Pill'; import Pill from "CommonUI/src/Components/Pill/Pill";
import FieldType from 'CommonUI/src/Components/Types/FieldType'; import FieldType from "CommonUI/src/Components/Types/FieldType";
import DropdownUtil from 'CommonUI/src/Utils/Dropdown'; import DropdownUtil from "CommonUI/src/Utils/Dropdown";
import ModelAPI from 'CommonUI/src/Utils/ModelAPI/ModelAPI'; import ModelAPI from "CommonUI/src/Utils/ModelAPI/ModelAPI";
import GlobalConfig, { EmailServerType } from 'Model/Models/GlobalConfig'; import GlobalConfig, { EmailServerType } from "Model/Models/GlobalConfig";
import React, { FunctionComponent, ReactElement, useEffect } from 'react'; import React, { FunctionComponent, ReactElement, useEffect } from "react";
const Settings: FunctionComponent = (): ReactElement => { const Settings: FunctionComponent = (): ReactElement => {
const [emailServerType, setemailServerType] = const [emailServerType, setemailServerType] = React.useState<EmailServerType>(
React.useState<EmailServerType>(EmailServerType.Internal); EmailServerType.Internal,
);
const [isLoading, setIsLoading] = React.useState<boolean>(true); const [isLoading, setIsLoading] = React.useState<boolean>(true);
const [error, setError] = React.useState<string>(''); const [error, setError] = React.useState<string>("");
const fetchItem: PromiseVoidFunction = async (): Promise<void> => { const fetchItem: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true); setIsLoading(true);
const globalConfig: GlobalConfig | null = const globalConfig: GlobalConfig | null =
await ModelAPI.getItem<GlobalConfig>({ await ModelAPI.getItem<GlobalConfig>({
modelType: GlobalConfig, modelType: GlobalConfig,
id: ObjectID.getZeroObjectID(), id: ObjectID.getZeroObjectID(),
select: { select: {
_id: true, _id: true,
emailServerType: true, emailServerType: true,
}, },
}); });
if (globalConfig) { if (globalConfig) {
setemailServerType( setemailServerType(
globalConfig.emailServerType || EmailServerType.Internal globalConfig.emailServerType || EmailServerType.Internal,
); );
}
setIsLoading(false);
};
useEffect(() => {
fetchItem().catch((err: Error) => {
setError(err.message);
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
} }
if (error) { setIsLoading(false);
return <ErrorMessage error={error} />; };
}
return ( useEffect(() => {
<Page fetchItem().catch((err: Error) => {
title={'Admin Settings'} setError(err.message);
breadcrumbLinks={[ });
{ }, []);
title: 'Admin Dashboard',
to: RouteUtil.populateRouteParams( if (isLoading) {
RouteMap[PageMap.HOME] as Route return <PageLoader isVisible={true} />;
), }
if (error) {
return <ErrorMessage error={error} />;
}
return (
<Page
title={"Admin Settings"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS] as Route,
),
},
{
title: "Email Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_SMTP] as Route,
),
},
]}
sideMenu={<DashboardSideMenu />}
>
{/* Project Settings View */}
<CardModelDetail
name="Admin Notification Email"
cardProps={{
title: "Admin Notification Email",
description:
"Enter the email address where you would like to receive admin-level notifications.",
}}
isEditable={true}
editButtonText="Edit Email"
formFields={[
{
field: {
adminNotificationEmail: true,
},
title: "Admin Notification Email",
fieldType: FormFieldSchemaType.Email,
required: false,
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: "model-detail-global-config",
fields: [
{
field: {
adminNotificationEmail: true,
},
title: "Admin Notification Email",
fieldType: FieldType.Email,
placeholder: "None",
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
<CardModelDetail
name="Internal SMTP Settings"
cardProps={{
title: "Email Server Settings",
description:
"Pick which email server you would like to use to send emails.",
}}
isEditable={true}
editButtonText="Edit Server"
onSaveSuccess={() => {
window.location.reload();
}}
formFields={[
{
field: {
emailServerType: true,
},
title: "Email Server Type",
fieldType: FormFieldSchemaType.Dropdown,
dropdownOptions:
DropdownUtil.getDropdownOptionsFromEnum(EmailServerType),
required: true,
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: "model-detail-global-config",
fields: [
{
field: {
emailServerType: true,
},
title: "Email Server Type",
fieldType: FieldType.Text,
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
{emailServerType === EmailServerType.CustomSMTP ? (
<CardModelDetail
name="Host Settings"
cardProps={{
title: "Custom Email and SMTP Settings",
description:
"If you have not enabled Internal SMTP server to send emails. Please configure your SMTP server here.",
}}
isEditable={true}
editButtonText="Edit SMTP Config"
formSteps={[
{
title: "SMTP Server",
id: "server-info",
},
{
title: "Authentication",
id: "authentication",
},
{
title: "Email",
id: "email-info",
},
]}
formFields={[
{
field: {
smtpHost: true,
},
title: "Hostname",
stepId: "server-info",
fieldType: FormFieldSchemaType.Hostname,
required: true,
placeholder: "smtp.server.com",
},
{
field: {
smtpPort: true,
},
title: "Port",
stepId: "server-info",
fieldType: FormFieldSchemaType.Port,
required: true,
placeholder: "587",
},
{
field: {
isSMTPSecure: true,
},
title: "Use SSL / TLS",
stepId: "server-info",
fieldType: FormFieldSchemaType.Toggle,
description:
"If you use port 465, please enable this. Do not enable this if you use port 587.",
},
{
field: {
smtpUsername: true,
},
title: "Username",
stepId: "authentication",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "emailuser",
},
{
field: {
smtpPassword: true,
},
title: "Password",
stepId: "authentication",
fieldType: FormFieldSchemaType.EncryptedText,
required: false,
placeholder: "Password",
},
{
field: {
smtpFromEmail: true,
},
title: "Email From",
stepId: "email-info",
fieldType: FormFieldSchemaType.Email,
required: true,
description:
"This is the display email your team and customers see, when they receive emails from OneUptime.",
placeholder: "email@company.com",
},
{
field: {
smtpFromName: true,
},
title: "From Name",
stepId: "email-info",
fieldType: FormFieldSchemaType.Text,
required: true,
description:
"This is the display name your team and customers see, when they receive emails from OneUptime.",
placeholder: "Company, Inc.",
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: "model-detail-global-config",
fields: [
{
field: {
smtpHost: true,
}, },
{ title: "SMTP Host",
title: 'Settings', placeholder: "None",
to: RouteUtil.populateRouteParams( },
RouteMap[PageMap.SETTINGS] as Route {
), field: {
smtpPort: true,
}, },
{ title: "SMTP Port",
title: 'Email Settings', placeholder: "None",
to: RouteUtil.populateRouteParams( },
RouteMap[PageMap.SETTINGS_SMTP] as Route {
), field: {
smtpUsername: true,
}, },
]} title: "SMTP Username",
sideMenu={<DashboardSideMenu />} placeholder: "None",
> },
{/* Project Settings View */} {
field: {
smtpFromEmail: true,
},
title: "SMTP Email",
placeholder: "None",
fieldType: FieldType.Email,
},
{
field: {
smtpFromName: true,
},
title: "SMTP From Name",
placeholder: "None",
},
{
field: {
isSMTPSecure: true,
},
title: "Use SSL/TLS",
placeholder: "No",
fieldType: FieldType.Boolean,
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
) : (
<></>
)}
<CardModelDetail {emailServerType === EmailServerType.Sendgrid ? (
name="Admin Notification Email" <CardModelDetail<GlobalConfig>
cardProps={{ name="Sendgrid Settings"
title: 'Admin Notification Email', cardProps={{
description: title: "Sendgrid Settings",
'Enter the email address where you would like to receive admin-level notifications.', description:
}} "Enter your Sendgrid API key to send emails through Sendgrid.",
isEditable={true} }}
editButtonText="Edit Email" isEditable={true}
formFields={[ editButtonText="Edit API Key"
{ formFields={[
field: { {
adminNotificationEmail: true, field: {
}, sendgridApiKey: true,
title: 'Admin Notification Email', },
fieldType: FormFieldSchemaType.Email, title: "Sendgrid API Key",
required: false, fieldType: FormFieldSchemaType.Text,
}, required: true,
]} placeholder: "Sendgrid API Key",
modelDetailProps={{ },
modelType: GlobalConfig, {
id: 'model-detail-global-config', field: {
fields: [ sendgridFromEmail: true,
{ },
field: { title: "From Email",
adminNotificationEmail: true, fieldType: FormFieldSchemaType.Email,
}, required: true,
title: 'Admin Notification Email', placeholder: "email@yourcompany.com",
fieldType: FieldType.Email, },
placeholder: 'None', {
}, field: {
], sendgridFromName: true,
modelId: ObjectID.getZeroObjectID(), },
}} title: "From Name",
/> fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "Acme, Inc.",
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: "model-detail-global-config",
selectMoreFields: {
sendgridFromEmail: true,
sendgridFromName: true,
},
fields: [
{
field: {
sendgridApiKey: true,
},
title: "",
placeholder: "None",
getElement: (item: GlobalConfig) => {
if (
item["sendgridApiKey"] &&
item["sendgridFromEmail"] &&
item["sendgridFromName"]
) {
return <Pill text="Enabled" color={Green} />;
} else if (!item["sendgridApiKey"]) {
return (
<Pill
text="Not Enabled. Please add the API key."
color={Red}
/>
);
} else if (!item["sendgridFromEmail"]) {
return (
<Pill
text="Not Enabled. Please add the From Email."
color={Red}
/>
);
} else if (!item["sendgridFromName"]) {
return (
<Pill
text="Not Enabled. Please add the From Name."
color={Red}
/>
);
}
<CardModelDetail return <></>;
name="Internal SMTP Settings" },
cardProps={{ },
title: 'Email Server Settings', ],
description: modelId: ObjectID.getZeroObjectID(),
'Pick which email server you would like to use to send emails.', }}
}} />
isEditable={true} ) : (
editButtonText="Edit Server" <></>
onSaveSuccess={() => { )}
window.location.reload(); </Page>
}} );
formFields={[
{
field: {
emailServerType: true,
},
title: 'Email Server Type',
fieldType: FormFieldSchemaType.Dropdown,
dropdownOptions:
DropdownUtil.getDropdownOptionsFromEnum(
EmailServerType
),
required: true,
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: 'model-detail-global-config',
fields: [
{
field: {
emailServerType: true,
},
title: 'Email Server Type',
fieldType: FieldType.Text,
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
{emailServerType === EmailServerType.CustomSMTP ? (
<CardModelDetail
name="Host Settings"
cardProps={{
title: 'Custom Email and SMTP Settings',
description:
'If you have not enabled Internal SMTP server to send emails. Please configure your SMTP server here.',
}}
isEditable={true}
editButtonText="Edit SMTP Config"
formSteps={[
{
title: 'SMTP Server',
id: 'server-info',
},
{
title: 'Authentication',
id: 'authentication',
},
{
title: 'Email',
id: 'email-info',
},
]}
formFields={[
{
field: {
smtpHost: true,
},
title: 'Hostname',
stepId: 'server-info',
fieldType: FormFieldSchemaType.Hostname,
required: true,
placeholder: 'smtp.server.com',
},
{
field: {
smtpPort: true,
},
title: 'Port',
stepId: 'server-info',
fieldType: FormFieldSchemaType.Port,
required: true,
placeholder: '587',
},
{
field: {
isSMTPSecure: true,
},
title: 'Use SSL / TLS',
stepId: 'server-info',
fieldType: FormFieldSchemaType.Toggle,
description:
'If you use port 465, please enable this. Do not enable this if you use port 587.',
},
{
field: {
smtpUsername: true,
},
title: 'Username',
stepId: 'authentication',
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: 'emailuser',
},
{
field: {
smtpPassword: true,
},
title: 'Password',
stepId: 'authentication',
fieldType: FormFieldSchemaType.EncryptedText,
required: false,
placeholder: 'Password',
},
{
field: {
smtpFromEmail: true,
},
title: 'Email From',
stepId: 'email-info',
fieldType: FormFieldSchemaType.Email,
required: true,
description:
'This is the display email your team and customers see, when they receive emails from OneUptime.',
placeholder: 'email@company.com',
},
{
field: {
smtpFromName: true,
},
title: 'From Name',
stepId: 'email-info',
fieldType: FormFieldSchemaType.Text,
required: true,
description:
'This is the display name your team and customers see, when they receive emails from OneUptime.',
placeholder: 'Company, Inc.',
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: 'model-detail-global-config',
fields: [
{
field: {
smtpHost: true,
},
title: 'SMTP Host',
placeholder: 'None',
},
{
field: {
smtpPort: true,
},
title: 'SMTP Port',
placeholder: 'None',
},
{
field: {
smtpUsername: true,
},
title: 'SMTP Username',
placeholder: 'None',
},
{
field: {
smtpFromEmail: true,
},
title: 'SMTP Email',
placeholder: 'None',
fieldType: FieldType.Email,
},
{
field: {
smtpFromName: true,
},
title: 'SMTP From Name',
placeholder: 'None',
},
{
field: {
isSMTPSecure: true,
},
title: 'Use SSL/TLS',
placeholder: 'No',
fieldType: FieldType.Boolean,
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
) : (
<></>
)}
{emailServerType === EmailServerType.Sendgrid ? (
<CardModelDetail<GlobalConfig>
name="Sendgrid Settings"
cardProps={{
title: 'Sendgrid Settings',
description:
'Enter your Sendgrid API key to send emails through Sendgrid.',
}}
isEditable={true}
editButtonText="Edit API Key"
formFields={[
{
field: {
sendgridApiKey: true,
},
title: 'Sendgrid API Key',
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: 'Sendgrid API Key',
},
{
field: {
sendgridFromEmail: true,
},
title: 'From Email',
fieldType: FormFieldSchemaType.Email,
required: true,
placeholder: 'email@yourcompany.com',
},
{
field: {
sendgridFromName: true,
},
title: 'From Name',
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: 'Acme, Inc.',
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: 'model-detail-global-config',
selectMoreFields: {
sendgridFromEmail: true,
sendgridFromName: true,
},
fields: [
{
field: {
sendgridApiKey: true,
},
title: '',
placeholder: 'None',
getElement: (item: GlobalConfig) => {
if (
item['sendgridApiKey'] &&
item['sendgridFromEmail'] &&
item['sendgridFromName']
) {
return (
<Pill
text="Enabled"
color={Green}
/>
);
} else if (!item['sendgridApiKey']) {
return (
<Pill
text="Not Enabled. Please add the API key."
color={Red}
/>
);
} else if (!item['sendgridFromEmail']) {
return (
<Pill
text="Not Enabled. Please add the From Email."
color={Red}
/>
);
} else if (!item['sendgridFromName']) {
return (
<Pill
text="Not Enabled. Please add the From Name."
color={Red}
/>
);
}
return <></>;
},
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
) : (
<></>
)}
</Page>
);
}; };
export default Settings; export default Settings;

View File

@@ -1,251 +1,243 @@
import AdminModelAPI from '../../../Utils/ModelAPI'; import AdminModelAPI from "../../../Utils/ModelAPI";
import PageMap from '../../../Utils/PageMap'; import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import DashboardSideMenu from '../SideMenu'; import DashboardSideMenu from "../SideMenu";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import IsNull from 'Common/Types/BaseDatabase/IsNull'; import IsNull from "Common/Types/BaseDatabase/IsNull";
import { Green, Red } from 'Common/Types/BrandColors'; import { Green, Red } from "Common/Types/BrandColors";
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import { ErrorFunction, VoidFunction } from 'Common/Types/FunctionTypes'; import { ErrorFunction, VoidFunction } from "Common/Types/FunctionTypes";
import Banner from 'CommonUI/src/Components/Banner/Banner'; import Banner from "CommonUI/src/Components/Banner/Banner";
import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button'; import { ButtonStyleType } from "CommonUI/src/Components/Button/Button";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal'; import ConfirmModal from "CommonUI/src/Components/Modal/ConfirmModal";
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable'; import ModelTable from "CommonUI/src/Components/ModelTable/ModelTable";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import ProbeElement from 'CommonUI/src/Components/Probe/Probe'; import ProbeElement from "CommonUI/src/Components/Probe/Probe";
import Statusbubble from 'CommonUI/src/Components/StatusBubble/StatusBubble'; import Statusbubble from "CommonUI/src/Components/StatusBubble/StatusBubble";
import FieldType from 'CommonUI/src/Components/Types/FieldType'; import FieldType from "CommonUI/src/Components/Types/FieldType";
import Probe from 'Model/Models/Probe'; import Probe from "Model/Models/Probe";
import React, { FunctionComponent, ReactElement, useState } from 'react'; import React, { FunctionComponent, ReactElement, useState } from "react";
const Settings: FunctionComponent = (): ReactElement => { const Settings: FunctionComponent = (): ReactElement => {
const [showKeyModal, setShowKeyModal] = useState<boolean>(false); const [showKeyModal, setShowKeyModal] = useState<boolean>(false);
const [currentProbe, setCurrentProbe] = useState<Probe | null>(null); const [currentProbe, setCurrentProbe] = useState<Probe | null>(null);
return ( return (
<Page <Page
title={'Admin Settings'} title={"Admin Settings"}
breadcrumbLinks={[ breadcrumbLinks={[
{ {
title: 'Admin Dashboard', title: "Admin Dashboard",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
RouteMap[PageMap.HOME] as Route },
), {
}, title: "Settings",
{ to: RouteUtil.populateRouteParams(
title: 'Settings', RouteMap[PageMap.SETTINGS] as Route,
to: RouteUtil.populateRouteParams( ),
RouteMap[PageMap.SETTINGS] as Route },
), {
}, title: "Global Probes",
{ to: RouteUtil.populateRouteParams(
title: 'Global Probes', RouteMap[PageMap.SETTINGS_PROBES] as Route,
to: RouteUtil.populateRouteParams( ),
RouteMap[PageMap.SETTINGS_PROBES] as Route },
), ]}
}, sideMenu={<DashboardSideMenu />}
]} >
sideMenu={<DashboardSideMenu />} {/* Project Settings View */}
>
{/* Project Settings View */}
<Banner <Banner
openInNewTab={true} openInNewTab={true}
title="Need help with setting up Global Probes?" title="Need help with setting up Global Probes?"
description="Here is a guide which will help you get set up" description="Here is a guide which will help you get set up"
link={Route.fromString('/docs/probe/custom-probe')} link={Route.fromString("/docs/probe/custom-probe")}
/> />
<ModelTable<Probe> <ModelTable<Probe>
modelType={Probe} modelType={Probe}
id="probes-table" id="probes-table"
name="Settings > Global Probes" name="Settings > Global Probes"
isDeleteable={true} isDeleteable={true}
isEditable={true} isEditable={true}
isCreateable={true} isCreateable={true}
cardProps={{ cardProps={{
title: 'Global Probes', title: "Global Probes",
description: description:
'Global Probes help you monitor external resources from different locations around the world.', "Global Probes help you monitor external resources from different locations around the world.",
}} }}
query={{ query={{
projectId: new IsNull(), projectId: new IsNull(),
isGlobalProbe: true, isGlobalProbe: true,
}} }}
modelAPI={AdminModelAPI} modelAPI={AdminModelAPI}
noItemsMessage={'No probes found.'} noItemsMessage={"No probes found."}
showRefreshButton={true} showRefreshButton={true}
onBeforeCreate={(item: Probe) => { onBeforeCreate={(item: Probe) => {
item.isGlobalProbe = true; item.isGlobalProbe = true;
return Promise.resolve(item); return Promise.resolve(item);
}} }}
formFields={[ formFields={[
{ {
field: { field: {
name: true, name: true,
}, },
title: 'Name', title: "Name",
fieldType: FormFieldSchemaType.Text, fieldType: FormFieldSchemaType.Text,
required: true, required: true,
placeholder: 'internal-probe', placeholder: "internal-probe",
validation: { validation: {
minLength: 2, minLength: 2,
}, },
}, },
{ {
field: { field: {
description: true, description: true,
}, },
title: 'Description', title: "Description",
fieldType: FormFieldSchemaType.LongText, fieldType: FormFieldSchemaType.LongText,
required: true, required: true,
placeholder: placeholder: "This probe is to monitor all the internal services.",
'This probe is to monitor all the internal services.', },
},
{ {
field: { field: {
iconFile: true, iconFile: true,
}, },
title: 'Probe Logo', title: "Probe Logo",
fieldType: FormFieldSchemaType.ImageFile, fieldType: FormFieldSchemaType.ImageFile,
required: false, required: false,
placeholder: 'Upload logo', placeholder: "Upload logo",
}, },
]} ]}
selectMoreFields={{ selectMoreFields={{
key: true, key: true,
iconFileId: true, iconFileId: true,
}} }}
actionButtons={[ actionButtons={[
{ {
title: 'Show ID and Key', title: "Show ID and Key",
buttonStyleType: ButtonStyleType.NORMAL, buttonStyleType: ButtonStyleType.NORMAL,
onClick: async ( onClick: async (
item: Probe, item: Probe,
onCompleteAction: VoidFunction, onCompleteAction: VoidFunction,
onError: ErrorFunction onError: ErrorFunction,
) => { ) => {
try { try {
setCurrentProbe(item); setCurrentProbe(item);
setShowKeyModal(true); setShowKeyModal(true);
onCompleteAction(); onCompleteAction();
} catch (err) { } catch (err) {
onCompleteAction(); onCompleteAction();
onError(err as Error); onError(err as Error);
} }
}, },
}, },
]} ]}
filters={[ filters={[
{ {
field: { field: {
name: true, name: true,
}, },
title: 'Name', title: "Name",
type: FieldType.Text, type: FieldType.Text,
}, },
{ {
field: { field: {
description: true, description: true,
}, },
title: 'Description', title: "Description",
type: FieldType.Text, type: FieldType.Text,
}, },
]} ]}
columns={[ columns={[
{ {
field: { field: {
name: true, name: true,
}, },
title: 'Name', title: "Name",
type: FieldType.Text, type: FieldType.Text,
getElement: (item: Probe): ReactElement => { getElement: (item: Probe): ReactElement => {
return <ProbeElement probe={item} />; return <ProbeElement probe={item} />;
}, },
}, },
{ {
field: { field: {
description: true, description: true,
}, },
title: 'Description', title: "Description",
type: FieldType.Text, type: FieldType.Text,
}, },
{ {
field: { field: {
lastAlive: true, lastAlive: true,
}, },
title: 'Status', title: "Status",
type: FieldType.Text, type: FieldType.Text,
getElement: (item: Probe): ReactElement => { getElement: (item: Probe): ReactElement => {
if ( if (
item && item &&
item['lastAlive'] && item["lastAlive"] &&
OneUptimeDate.getNumberOfMinutesBetweenDates( OneUptimeDate.getNumberOfMinutesBetweenDates(
OneUptimeDate.fromString(item['lastAlive']), OneUptimeDate.fromString(item["lastAlive"]),
OneUptimeDate.getCurrentDate() OneUptimeDate.getCurrentDate(),
) < 5 ) < 5
) { ) {
return ( return (
<Statusbubble <Statusbubble
text={'Connected'} text={"Connected"}
color={Green} color={Green}
shouldAnimate={true} shouldAnimate={true}
/> />
); );
} }
return ( return (
<Statusbubble <Statusbubble
text={'Disconnected'} text={"Disconnected"}
color={Red} color={Red}
shouldAnimate={false} shouldAnimate={false}
/>
);
},
},
]}
/>
{showKeyModal && currentProbe ? (
<ConfirmModal
title={`Probe Key`}
description={
<div>
<span>
Here is your probe key. Please keep this a
secret.
</span>
<br />
<br />
<span>
<b>Probe ID: </b>{' '}
{currentProbe['_id']?.toString()}
</span>
<br />
<br />
<span>
<b>Probe Key: </b>{' '}
{currentProbe['key']?.toString()}
</span>
</div>
}
submitButtonText={'Close'}
submitButtonType={ButtonStyleType.NORMAL}
onSubmit={async () => {
setShowKeyModal(false);
}}
/> />
) : ( );
<></> },
)} },
</Page> ]}
); />
{showKeyModal && currentProbe ? (
<ConfirmModal
title={`Probe Key`}
description={
<div>
<span>Here is your probe key. Please keep this a secret.</span>
<br />
<br />
<span>
<b>Probe ID: </b> {currentProbe["_id"]?.toString()}
</span>
<br />
<br />
<span>
<b>Probe Key: </b> {currentProbe["key"]?.toString()}
</span>
</div>
}
submitButtonText={"Close"}
submitButtonType={ButtonStyleType.NORMAL}
onSubmit={async () => {
setShowKeyModal(false);
}}
/>
) : (
<></>
)}
</Page>
);
}; };
export default Settings; export default Settings;

View File

@@ -1,17 +1,17 @@
import PageMap from '../../Utils/PageMap'; import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import IconProp from 'Common/Types/Icon/IconProp'; import IconProp from "Common/Types/Icon/IconProp";
import SideMenu from 'CommonUI/src/Components/SideMenu/SideMenu'; import SideMenu from "CommonUI/src/Components/SideMenu/SideMenu";
import SideMenuItem from 'CommonUI/src/Components/SideMenu/SideMenuItem'; import SideMenuItem from "CommonUI/src/Components/SideMenu/SideMenuItem";
import SideMenuSection from 'CommonUI/src/Components/SideMenu/SideMenuSection'; import SideMenuSection from "CommonUI/src/Components/SideMenu/SideMenuSection";
import React, { ReactElement } from 'react'; import React, { ReactElement } from "react";
const DashboardSideMenu: () => JSX.Element = (): ReactElement => { const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
return ( return (
<SideMenu> <SideMenu>
<SideMenuSection title="Basic"> <SideMenuSection title="Basic">
{/* <SideMenuItem {/* <SideMenuItem
link={{ link={{
title: 'Host', title: 'Host',
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(
@@ -20,62 +20,62 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
}} }}
icon={IconProp.Globe} icon={IconProp.Globe}
/> */} /> */}
<SideMenuItem <SideMenuItem
link={{ link={{
title: 'Authentication', title: "Authentication",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_AUTHENTICATION] as Route RouteMap[PageMap.SETTINGS_AUTHENTICATION] as Route,
), ),
}} }}
icon={IconProp.Lock} icon={IconProp.Lock}
/> />
</SideMenuSection> </SideMenuSection>
<SideMenuSection title="Notifications"> <SideMenuSection title="Notifications">
<SideMenuItem <SideMenuItem
link={{ link={{
title: 'Emails', title: "Emails",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_SMTP] as Route RouteMap[PageMap.SETTINGS_SMTP] as Route,
), ),
}} }}
icon={IconProp.Email} icon={IconProp.Email}
/> />
<SideMenuItem <SideMenuItem
link={{ link={{
title: 'Call and SMS', title: "Call and SMS",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_CALL_AND_SMS] as Route RouteMap[PageMap.SETTINGS_CALL_AND_SMS] as Route,
), ),
}} }}
icon={IconProp.Call} icon={IconProp.Call}
/> />
</SideMenuSection> </SideMenuSection>
<SideMenuSection title="Monitoring"> <SideMenuSection title="Monitoring">
<SideMenuItem <SideMenuItem
link={{ link={{
title: 'Global Probes', title: "Global Probes",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_PROBES] as Route RouteMap[PageMap.SETTINGS_PROBES] as Route,
), ),
}} }}
icon={IconProp.Signal} icon={IconProp.Signal}
/> />
</SideMenuSection> </SideMenuSection>
<SideMenuSection title="API and Integrations"> <SideMenuSection title="API and Integrations">
<SideMenuItem <SideMenuItem
link={{ link={{
title: 'API Key', title: "API Key",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_API_KEY] as Route RouteMap[PageMap.SETTINGS_API_KEY] as Route,
), ),
}} }}
icon={IconProp.Code} icon={IconProp.Code}
/> />
</SideMenuSection> </SideMenuSection>
</SideMenu> </SideMenu>
); );
}; };
export default DashboardSideMenu; export default DashboardSideMenu;

View File

@@ -1,222 +1,218 @@
import PageMap from '../../Utils/PageMap'; import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import { ErrorFunction } from 'Common/Types/FunctionTypes'; import { ErrorFunction } from "Common/Types/FunctionTypes";
import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button'; import { ButtonStyleType } from "CommonUI/src/Components/Button/Button";
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal'; import ConfirmModal from "CommonUI/src/Components/Modal/ConfirmModal";
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable'; import ModelTable from "CommonUI/src/Components/ModelTable/ModelTable";
import Page from 'CommonUI/src/Components/Page/Page'; import Page from "CommonUI/src/Components/Page/Page";
import FieldType from 'CommonUI/src/Components/Types/FieldType'; import FieldType from "CommonUI/src/Components/Types/FieldType";
import API from 'CommonUI/src/Utils/API/API'; import API from "CommonUI/src/Utils/API/API";
import ModelAPI from 'CommonUI/src/Utils/ModelAPI/ModelAPI'; import ModelAPI from "CommonUI/src/Utils/ModelAPI/ModelAPI";
import Navigation from 'CommonUI/src/Utils/Navigation'; import Navigation from "CommonUI/src/Utils/Navigation";
import User from 'Model/Models/User'; import User from "Model/Models/User";
import React, { FunctionComponent, ReactElement, useState } from 'react'; import React, { FunctionComponent, ReactElement, useState } from "react";
const Users: FunctionComponent = (): ReactElement => { const Users: FunctionComponent = (): ReactElement => {
const [showConfirmVerifyEmailModal, setShowConfirmVerifyEmailModal] = const [showConfirmVerifyEmailModal, setShowConfirmVerifyEmailModal] =
useState<boolean>(false); useState<boolean>(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null); const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isConfimModalLoading, setIsConfirmModalLoading] = const [isConfimModalLoading, setIsConfirmModalLoading] =
useState<boolean>(false); useState<boolean>(false);
const [refreshItemsTrigger, setRefreshItemsTrigger] = const [refreshItemsTrigger, setRefreshItemsTrigger] =
useState<boolean>(false); useState<boolean>(false);
return ( return (
<Page <Page
title={'Users'} title={"Users"}
breadcrumbLinks={[ breadcrumbLinks={[
{ {
title: 'Admin Dashboard', title: "Admin Dashboard",
to: RouteUtil.populateRouteParams( to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
RouteMap[PageMap.HOME] as Route },
), {
title: "Users",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route),
},
]}
>
<ModelTable<User>
modelType={User}
id="users-table"
isDeleteable={false}
isEditable={false}
showViewIdButton={true}
refreshToggle={refreshItemsTrigger}
isCreateable={true}
name="Users"
isViewable={false}
cardProps={{
title: "Users",
description: "Here is a list of users in OneUptime.",
}}
actionButtons={[
{
title: "Verify Email",
buttonStyleType: ButtonStyleType.NORMAL,
isVisible: (item: User) => {
return !item.isEmailVerified;
},
onClick: async (
item: User,
onCompleteAction: VoidFunction,
onError: ErrorFunction,
) => {
try {
setSelectedUser(item);
setShowConfirmVerifyEmailModal(true);
onCompleteAction();
} catch (err) {
onCompleteAction();
onError(err as Error);
}
},
},
]}
noItemsMessage={"No users found."}
formFields={[
{
field: {
email: true,
},
title: "Email",
fieldType: FormFieldSchemaType.Email,
required: true,
placeholder: "email@company.com",
},
{
field: {
password: true,
},
title: "Password",
fieldType: FormFieldSchemaType.Password,
required: true,
placeholder: "Password",
},
{
field: {
name: true,
},
title: "Full Name",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "John Smith",
},
]}
showRefreshButton={true}
viewPageRoute={Navigation.getCurrentRoute()}
filters={[
{
field: {
name: true,
},
title: "Full Name",
type: FieldType.Text,
},
{
field: {
email: true,
},
title: "Email",
type: FieldType.Email,
},
{
field: {
createdAt: true,
},
title: "Created At",
type: FieldType.DateTime,
},
]}
columns={[
{
field: {
name: true,
},
title: "Full Name",
type: FieldType.Text,
},
{
field: {
email: true,
},
title: "Email",
type: FieldType.Email,
},
{
field: {
isEmailVerified: true,
},
title: "Email Verified",
type: FieldType.Boolean,
},
{
field: {
createdAt: true,
},
title: "Created At",
type: FieldType.DateTime,
},
]}
/>
{error ? (
<ConfirmModal
title={`Error`}
description={error}
submitButtonText={"Close"}
onSubmit={async () => {
setError(null);
}}
submitButtonType={ButtonStyleType.NORMAL}
/>
) : (
<></>
)}
{showConfirmVerifyEmailModal && selectedUser ? (
<ConfirmModal
title={`Verify Email`}
description={`Are you sure you want to verify the email - ${selectedUser.email}?`}
isLoading={isConfimModalLoading}
submitButtonText={"Verify Email"}
onClose={async () => {
setShowConfirmVerifyEmailModal(false);
setSelectedUser(null);
}}
onSubmit={async () => {
try {
setIsConfirmModalLoading(true);
await ModelAPI.updateById<User>({
modelType: User,
id: selectedUser.id!,
data: {
isEmailVerified: true,
}, },
{ });
title: 'Users', } catch (err) {
to: RouteUtil.populateRouteParams( setError(API.getFriendlyMessage(err as Error));
RouteMap[PageMap.USERS] as Route }
),
},
]}
>
<ModelTable<User>
modelType={User}
id="users-table"
isDeleteable={false}
isEditable={false}
showViewIdButton={true}
refreshToggle={refreshItemsTrigger}
isCreateable={true}
name="Users"
isViewable={false}
cardProps={{
title: 'Users',
description: 'Here is a list of users in OneUptime.',
}}
actionButtons={[
{
title: 'Verify Email',
buttonStyleType: ButtonStyleType.NORMAL,
isVisible: (item: User) => {
return !item.isEmailVerified;
},
onClick: async (
item: User,
onCompleteAction: VoidFunction,
onError: ErrorFunction
) => {
try {
setSelectedUser(item);
setShowConfirmVerifyEmailModal(true);
onCompleteAction(); setRefreshItemsTrigger(!refreshItemsTrigger);
} catch (err) { setIsConfirmModalLoading(false);
onCompleteAction(); setShowConfirmVerifyEmailModal(false);
onError(err as Error); }}
} />
}, ) : (
}, <></>
]} )}
noItemsMessage={'No users found.'} </Page>
formFields={[ );
{
field: {
email: true,
},
title: 'Email',
fieldType: FormFieldSchemaType.Email,
required: true,
placeholder: 'email@company.com',
},
{
field: {
password: true,
},
title: 'Password',
fieldType: FormFieldSchemaType.Password,
required: true,
placeholder: 'Password',
},
{
field: {
name: true,
},
title: 'Full Name',
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: 'John Smith',
},
]}
showRefreshButton={true}
viewPageRoute={Navigation.getCurrentRoute()}
filters={[
{
field: {
name: true,
},
title: 'Full Name',
type: FieldType.Text,
},
{
field: {
email: true,
},
title: 'Email',
type: FieldType.Email,
},
{
field: {
createdAt: true,
},
title: 'Created At',
type: FieldType.DateTime,
},
]}
columns={[
{
field: {
name: true,
},
title: 'Full Name',
type: FieldType.Text,
},
{
field: {
email: true,
},
title: 'Email',
type: FieldType.Email,
},
{
field: {
isEmailVerified: true,
},
title: 'Email Verified',
type: FieldType.Boolean,
},
{
field: {
createdAt: true,
},
title: 'Created At',
type: FieldType.DateTime,
},
]}
/>
{error ? (
<ConfirmModal
title={`Error`}
description={error}
submitButtonText={'Close'}
onSubmit={async () => {
setError(null);
}}
submitButtonType={ButtonStyleType.NORMAL}
/>
) : (
<></>
)}
{showConfirmVerifyEmailModal && selectedUser ? (
<ConfirmModal
title={`Verify Email`}
description={`Are you sure you want to verify the email - ${selectedUser.email}?`}
isLoading={isConfimModalLoading}
submitButtonText={'Verify Email'}
onClose={async () => {
setShowConfirmVerifyEmailModal(false);
setSelectedUser(null);
}}
onSubmit={async () => {
try {
setIsConfirmModalLoading(true);
await ModelAPI.updateById<User>({
modelType: User,
id: selectedUser.id!,
data: {
isEmailVerified: true,
},
});
} catch (err) {
setError(API.getFriendlyMessage(err as Error));
}
setRefreshItemsTrigger(!refreshItemsTrigger);
setIsConfirmModalLoading(false);
setShowConfirmVerifyEmailModal(false);
}}
/>
) : (
<></>
)}
</Page>
);
}; };
export default Users; export default Users;

View File

@@ -1,8 +1,8 @@
import Dictionary from 'Common/Types/Dictionary'; import Dictionary from "Common/Types/Dictionary";
import ModelAPI from 'CommonUI/src/Utils/ModelAPI/ModelAPI'; import ModelAPI from "CommonUI/src/Utils/ModelAPI/ModelAPI";
export default class AdminModelAPI extends ModelAPI { export default class AdminModelAPI extends ModelAPI {
public static override getCommonHeaders(): Dictionary<string> { public static override getCommonHeaders(): Dictionary<string> {
return {}; return {};
} }
} }

View File

@@ -1,17 +1,17 @@
enum PageMap { enum PageMap {
INIT = 'INIT', INIT = "INIT",
HOME = 'HOME', HOME = "HOME",
LOGOUT = 'LOGOUT', LOGOUT = "LOGOUT",
SETTINGS = 'SETTINGS', SETTINGS = "SETTINGS",
USERS = 'USERS', USERS = "USERS",
PROJECTS = 'PROJECTS', PROJECTS = "PROJECTS",
SETTINGS_HOST = 'SETTINGS_HOST', SETTINGS_HOST = "SETTINGS_HOST",
SETTINGS_SMTP = 'SETTINGS_SMTP', SETTINGS_SMTP = "SETTINGS_SMTP",
SETTINGS_CALL_AND_SMS = 'SETTINGS_CALL_AND_SMS', SETTINGS_CALL_AND_SMS = "SETTINGS_CALL_AND_SMS",
SETTINGS_PROBES = 'SETTINGS_PROBES', SETTINGS_PROBES = "SETTINGS_PROBES",
SETTINGS_AUTHENTICATION = 'SETTINGS_AUTHENTICATION', SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
SETTINGS_API_KEY = 'SETTINGS_API_KEY', SETTINGS_API_KEY = "SETTINGS_API_KEY",
} }
export default PageMap; export default PageMap;

View File

@@ -1,54 +1,54 @@
import PageMap from './PageMap'; import PageMap from "./PageMap";
import RouteParams from './RouteParams'; import RouteParams from "./RouteParams";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import Dictionary from 'Common/Types/Dictionary'; import Dictionary from "Common/Types/Dictionary";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
const RouteMap: Dictionary<Route> = { const RouteMap: Dictionary<Route> = {
[PageMap.INIT]: new Route(`/admin`), [PageMap.INIT]: new Route(`/admin`),
[PageMap.HOME]: new Route(`/admin`), [PageMap.HOME]: new Route(`/admin`),
[PageMap.LOGOUT]: new Route(`/admin/logout`), [PageMap.LOGOUT]: new Route(`/admin/logout`),
[PageMap.SETTINGS]: new Route(`/admin/settings/host`), [PageMap.SETTINGS]: new Route(`/admin/settings/host`),
[PageMap.PROJECTS]: new Route(`/admin/projects`), [PageMap.PROJECTS]: new Route(`/admin/projects`),
[PageMap.USERS]: new Route(`/admin/users`), [PageMap.USERS]: new Route(`/admin/users`),
[PageMap.SETTINGS_HOST]: new Route(`/admin/settings/host`), [PageMap.SETTINGS_HOST]: new Route(`/admin/settings/host`),
[PageMap.SETTINGS_SMTP]: new Route(`/admin/settings/smtp`), [PageMap.SETTINGS_SMTP]: new Route(`/admin/settings/smtp`),
[PageMap.SETTINGS_CALL_AND_SMS]: new Route(`/admin/settings/call-and-sms`), [PageMap.SETTINGS_CALL_AND_SMS]: new Route(`/admin/settings/call-and-sms`),
[PageMap.SETTINGS_PROBES]: new Route(`/admin/settings/probes`), [PageMap.SETTINGS_PROBES]: new Route(`/admin/settings/probes`),
[PageMap.SETTINGS_AUTHENTICATION]: new Route( [PageMap.SETTINGS_AUTHENTICATION]: new Route(
`/admin/settings/authentication` `/admin/settings/authentication`,
), ),
[PageMap.SETTINGS_API_KEY]: new Route(`/admin/settings/api-key`), [PageMap.SETTINGS_API_KEY]: new Route(`/admin/settings/api-key`),
}; };
export class RouteUtil { export class RouteUtil {
public static populateRouteParams( public static populateRouteParams(
route: Route, route: Route,
props?: { props?: {
modelId?: ObjectID; modelId?: ObjectID;
subModelId?: ObjectID; subModelId?: ObjectID;
} },
): Route { ): Route {
// populate projectid // populate projectid
const tempRoute: Route = new Route(route.toString()); const tempRoute: Route = new Route(route.toString());
if (props && props.modelId) { if (props && props.modelId) {
route = tempRoute.addRouteParam( route = tempRoute.addRouteParam(
RouteParams.ModelID, RouteParams.ModelID,
props.modelId.toString() props.modelId.toString(),
); );
}
if (props && props.subModelId) {
route = tempRoute.addRouteParam(
RouteParams.SubModelID,
props.subModelId.toString()
);
}
return tempRoute;
} }
if (props && props.subModelId) {
route = tempRoute.addRouteParam(
RouteParams.SubModelID,
props.subModelId.toString(),
);
}
return tempRoute;
}
} }
export default RouteMap; export default RouteMap;

View File

@@ -1,6 +1,6 @@
enum RouteParams { enum RouteParams {
ModelID = ':id', ModelID = ":id",
SubModelID = ':subModelId', SubModelID = ":subModelId",
} }
export default RouteParams; export default RouteParams;

View File

@@ -1,82 +1,84 @@
require('ts-loader'); require("ts-loader");
require('file-loader'); require("file-loader");
require('style-loader'); require("style-loader");
require('css-loader'); require("css-loader");
require('sass-loader'); require("sass-loader");
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const dotenv = require('dotenv'); const dotenv = require("dotenv");
const express = require('express'); const express = require("express");
const readEnvFile = (pathToFile) => { const readEnvFile = (pathToFile) => {
const parsed = dotenv.config({ path: pathToFile }).parsed;
const parsed = dotenv.config({ path: pathToFile }).parsed; const env = {};
const env = {}; for (const key in parsed) {
env[key] = JSON.stringify(parsed[key]);
}
for (const key in parsed) { return env;
env[key] = JSON.stringify(parsed[key]); };
}
return env;
}
module.exports = { module.exports = {
entry: "./src/Index.tsx", entry: "./src/Index.tsx",
mode: "development", mode: "development",
output: { output: {
filename: "bundle.js", filename: "bundle.js",
path: path.resolve(__dirname, "public", "dist"), path: path.resolve(__dirname, "public", "dist"),
publicPath: "/admin/dist/", publicPath: "/admin/dist/",
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".scss"],
alias: {
react: path.resolve("./node_modules/react"),
}, },
resolve: { },
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.css', '.scss'], externals: {
alias: { "react-native-sqlite-storage": "react-native-sqlite-storage",
react: path.resolve('./node_modules/react'), },
} plugins: [
}, new webpack.DefinePlugin({
externals: { process: {
'react-native-sqlite-storage': 'react-native-sqlite-storage' env: {
}, ...readEnvFile("/usr/src/app/dev-env/.env"),
plugins: [
new webpack.DefinePlugin({
'process': {
'env': {
...readEnvFile('/usr/src/app/dev-env/.env')
}
}
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader'
},
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', "sass-loader"]
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loader: 'file-loader'
}
],
},
devServer: {
historyApiFallback: true,
devMiddleware: {
writeToDisk: true,
}, },
allowedHosts: "all", },
setupMiddlewares: (middlewares, devServer) => { }),
devServer.app.use('/admin/assets', express.static(path.resolve(__dirname, 'public', 'assets'))); ],
return middlewares; module: {
} rules: [
{
test: /\.(ts|tsx)$/,
use: "ts-loader",
},
{
test: /\.s[ac]ss$/i,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loader: "file-loader",
},
],
},
devServer: {
historyApiFallback: true,
devMiddleware: {
writeToDisk: true,
}, },
devtool: 'eval-source-map', allowedHosts: "all",
} setupMiddlewares: (middlewares, devServer) => {
devServer.app.use(
"/admin/assets",
express.static(path.resolve(__dirname, "public", "assets")),
);
return middlewares;
},
},
devtool: "eval-source-map",
};

View File

@@ -1,89 +1,83 @@
import AuthenticationServiceHandler from './Service/Authentication'; import AuthenticationServiceHandler from "./Service/Authentication";
import DataTypeServiceHandler from './Service/DataType'; import DataTypeServiceHandler from "./Service/DataType";
import ErrorServiceHandler from './Service/Errors'; import ErrorServiceHandler from "./Service/Errors";
import IntroductionServiceHandler from './Service/Introduction'; import IntroductionServiceHandler from "./Service/Introduction";
import ModelServiceHandler from './Service/Model'; import ModelServiceHandler from "./Service/Model";
import PageNotFoundServiceHandler from './Service/PageNotFound'; import PageNotFoundServiceHandler from "./Service/PageNotFound";
import PaginationServiceHandler from './Service/Pagination'; import PaginationServiceHandler from "./Service/Pagination";
import PermissionServiceHandler from './Service/Permissions'; import PermissionServiceHandler from "./Service/Permissions";
import StatusServiceHandler from './Service/Status'; import StatusServiceHandler from "./Service/Status";
import { StaticPath } from './Utils/Config'; import { StaticPath } from "./Utils/Config";
import ResourceUtil, { ModelDocumentation } from './Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "./Utils/Resources";
import Dictionary from 'Common/Types/Dictionary'; import Dictionary from "Common/Types/Dictionary";
import FeatureSet from 'CommonServer/Types/FeatureSet'; import FeatureSet from "CommonServer/Types/FeatureSet";
import Express, { import Express, {
ExpressApplication, ExpressApplication,
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressStatic, ExpressStatic,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
const APIReferenceFeatureSet: FeatureSet = { const APIReferenceFeatureSet: FeatureSet = {
init: async (): Promise<void> => { init: async (): Promise<void> => {
const ResourceDictionary: Dictionary<ModelDocumentation> = const ResourceDictionary: Dictionary<ModelDocumentation> =
ResourceUtil.getResourceDictionaryByPath(); ResourceUtil.getResourceDictionaryByPath();
const app: ExpressApplication = Express.getExpressApp(); const app: ExpressApplication = Express.getExpressApp();
app.use('/reference', ExpressStatic(StaticPath, { maxAge: 2592000 })); app.use("/reference", ExpressStatic(StaticPath, { maxAge: 2592000 }));
// Index page // Index page
app.get( app.get(["/reference"], (_req: ExpressRequest, res: ExpressResponse) => {
['/reference'], return res.redirect("/reference/introduction");
(_req: ExpressRequest, res: ExpressResponse) => { });
return res.redirect('/reference/introduction');
}
);
app.get( app.get(
['/reference/page-not-found'], ["/reference/page-not-found"],
(req: ExpressRequest, res: ExpressResponse) => { (req: ExpressRequest, res: ExpressResponse) => {
return PageNotFoundServiceHandler.executeResponse(req, res); return PageNotFoundServiceHandler.executeResponse(req, res);
} },
); );
// All Pages // All Pages
app.get( app.get(
['/reference/:page'], ["/reference/:page"],
(req: ExpressRequest, res: ExpressResponse) => { (req: ExpressRequest, res: ExpressResponse) => {
const page: string | undefined = req.params['page']; const page: string | undefined = req.params["page"];
if (!page) { if (!page) {
return PageNotFoundServiceHandler.executeResponse(req, res); return PageNotFoundServiceHandler.executeResponse(req, res);
} }
const currentResource: ModelDocumentation | undefined = const currentResource: ModelDocumentation | undefined =
ResourceDictionary[page]; ResourceDictionary[page];
if (req.params['page'] === 'permissions') { if (req.params["page"] === "permissions") {
return PermissionServiceHandler.executeResponse(req, res); return PermissionServiceHandler.executeResponse(req, res);
} else if (req.params['page'] === 'authentication') { } else if (req.params["page"] === "authentication") {
return AuthenticationServiceHandler.executeResponse( return AuthenticationServiceHandler.executeResponse(req, res);
req, } else if (req.params["page"] === "pagination") {
res return PaginationServiceHandler.executeResponse(req, res);
); } else if (req.params["page"] === "errors") {
} else if (req.params['page'] === 'pagination') { return ErrorServiceHandler.executeResponse(req, res);
return PaginationServiceHandler.executeResponse(req, res); } else if (req.params["page"] === "introduction") {
} else if (req.params['page'] === 'errors') { return IntroductionServiceHandler.executeResponse(req, res);
return ErrorServiceHandler.executeResponse(req, res); } else if (req.params["page"] === "status") {
} else if (req.params['page'] === 'introduction') { return StatusServiceHandler.executeResponse(req, res);
return IntroductionServiceHandler.executeResponse(req, res); } else if (req.params["page"] === "data-types") {
} else if (req.params['page'] === 'status') { return DataTypeServiceHandler.executeResponse(req, res);
return StatusServiceHandler.executeResponse(req, res); } else if (currentResource) {
} else if (req.params['page'] === 'data-types') { return ModelServiceHandler.executeResponse(req, res);
return DataTypeServiceHandler.executeResponse(req, res); }
} else if (currentResource) { // page not found
return ModelServiceHandler.executeResponse(req, res); return PageNotFoundServiceHandler.executeResponse(req, res);
} },
// page not found );
return PageNotFoundServiceHandler.executeResponse(req, res);
}
);
app.get('/reference/*', (req: ExpressRequest, res: ExpressResponse) => { app.get("/reference/*", (req: ExpressRequest, res: ExpressResponse) => {
return PageNotFoundServiceHandler.executeResponse(req, res); return PageNotFoundServiceHandler.executeResponse(req, res);
}); });
}, },
}; };
export default APIReferenceFeatureSet; export default APIReferenceFeatureSet;

View File

@@ -1,29 +1,28 @@
import { ViewsPath } from '../Utils/Config'; import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
let pageTitle: string = ''; let pageTitle: string = "";
let pageDescription: string = ''; let pageDescription: string = "";
const page: string | undefined = req.params['page']; const page: string | undefined = req.params["page"];
const pageData: any = {}; const pageData: any = {};
pageTitle = 'Authentication'; pageTitle = "Authentication";
pageDescription = pageDescription = "Learn how to authenticate requests with OneUptime API";
'Learn how to authenticate requests with OneUptime API';
return res.render(`${ViewsPath}/pages/index`, { return res.render(`${ViewsPath}/pages/index`, {
page: page, page: page,
resources: Resources, resources: Resources,
pageTitle: pageTitle, pageTitle: pageTitle,
pageDescription: pageDescription, pageDescription: pageDescription,
pageData: pageData, pageData: pageData,
}); });
} }
} }

View File

@@ -1,136 +1,126 @@
import { CodeExamplesPath, ViewsPath } from '../Utils/Config'; import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import LocalCache from 'CommonServer/Infrastructure/LocalCache'; import LocalCache from "CommonServer/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
import LocalFile from 'CommonServer/Utils/LocalFile'; import LocalFile from "CommonServer/Utils/LocalFile";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
_req: ExpressRequest, _req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
const pageData: any = {}; const pageData: any = {};
pageData.selectCode = await LocalCache.getOrSetString( pageData.selectCode = await LocalCache.getOrSetString(
'data-type', "data-type",
'select', "select",
async () => { async () => {
return await LocalFile.read( return await LocalFile.read(`${CodeExamplesPath}/DataTypes/Select.md`);
`${CodeExamplesPath}/DataTypes/Select.md` },
); );
}
pageData.sortCode = await LocalCache.getOrSetString(
"data-type",
"sort",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/DataTypes/Sort.md`);
},
);
pageData.equalToCode = await LocalCache.getOrSetString(
"data-type",
"equal-to",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/DataTypes/EqualTo.md`);
},
);
pageData.equalToOrNullCode = await LocalCache.getOrSetString(
"data-type",
"equal-to-or-null",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/EqualToOrNull.md`,
); );
},
);
pageData.sortCode = await LocalCache.getOrSetString( pageData.greaterThanCode = await LocalCache.getOrSetString(
'data-type', "data-type",
'sort', "greater-than",
async () => { async () => {
return await LocalFile.read( return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/Sort.md` `${CodeExamplesPath}/DataTypes/GreaterThan.md`,
);
}
); );
},
);
pageData.equalToCode = await LocalCache.getOrSetString( pageData.greaterThanOrEqualCode = await LocalCache.getOrSetString(
'data-type', "data-type",
'equal-to', "greater-than-or-equal",
async () => { async () => {
return await LocalFile.read( return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/EqualTo.md` `${CodeExamplesPath}/DataTypes/GreaterThanOrEqual.md`,
);
}
); );
},
);
pageData.equalToOrNullCode = await LocalCache.getOrSetString( pageData.lessThanCode = await LocalCache.getOrSetString(
'data-type', "data-type",
'equal-to-or-null', "less-than",
async () => { async () => {
return await LocalFile.read( return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/EqualToOrNull.md` `${CodeExamplesPath}/DataTypes/LessThan.md`,
);
}
); );
},
);
pageData.greaterThanCode = await LocalCache.getOrSetString( pageData.lessThanOrEqualCode = await LocalCache.getOrSetString(
'data-type', "data-type",
'greater-than', "less-than-or-equal",
async () => { async () => {
return await LocalFile.read( return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/GreaterThan.md` `${CodeExamplesPath}/DataTypes/LessThanOrEqual.md`,
);
}
); );
},
);
pageData.greaterThanOrEqualCode = await LocalCache.getOrSetString( pageData.isNullCode = await LocalCache.getOrSetString(
'data-type', "data-type",
'greater-than-or-equal', "is-null",
async () => { async () => {
return await LocalFile.read( return await LocalFile.read(`${CodeExamplesPath}/DataTypes/IsNull.md`);
`${CodeExamplesPath}/DataTypes/GreaterThanOrEqual.md` },
); );
}
pageData.notNullCode = await LocalCache.getOrSetString(
"data-type",
"not-null",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/DataTypes/NotNull.md`);
},
);
pageData.notEqualToCode = await LocalCache.getOrSetString(
"data-type",
"not-equals",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/NotEqualTo.md`,
); );
},
);
pageData.lessThanCode = await LocalCache.getOrSetString( res.status(200);
'data-type', return res.render(`${ViewsPath}/pages/index`, {
'less-than', page: "data-types",
async () => { pageTitle: "Data Types",
return await LocalFile.read( pageDescription:
`${CodeExamplesPath}/DataTypes/LessThan.md` "Data Types that can be used to interact with OneUptime API",
); resources: Resources,
} pageData: pageData,
); });
}
pageData.lessThanOrEqualCode = await LocalCache.getOrSetString(
'data-type',
'less-than-or-equal',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/LessThanOrEqual.md`
);
}
);
pageData.isNullCode = await LocalCache.getOrSetString(
'data-type',
'is-null',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/IsNull.md`
);
}
);
pageData.notNullCode = await LocalCache.getOrSetString(
'data-type',
'not-null',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/NotNull.md`
);
}
);
pageData.notEqualToCode = await LocalCache.getOrSetString(
'data-type',
'not-equals',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/NotEqualTo.md`
);
}
);
res.status(200);
return res.render(`${ViewsPath}/pages/index`, {
page: 'data-types',
pageTitle: 'Data Types',
pageDescription:
'Data Types that can be used to interact with OneUptime API',
resources: Resources,
pageData: pageData,
});
}
} }

View File

@@ -1,28 +1,28 @@
import { ViewsPath } from '../Utils/Config'; import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
let pageTitle: string = ''; let pageTitle: string = "";
let pageDescription: string = ''; let pageDescription: string = "";
const page: string | undefined = req.params['page']; const page: string | undefined = req.params["page"];
const pageData: any = {}; const pageData: any = {};
pageTitle = 'Errors'; pageTitle = "Errors";
pageDescription = 'Learn more about how we return errors from API'; pageDescription = "Learn more about how we return errors from API";
return res.render(`${ViewsPath}/pages/index`, { return res.render(`${ViewsPath}/pages/index`, {
page: page, page: page,
resources: Resources, resources: Resources,
pageTitle: pageTitle, pageTitle: pageTitle,
pageDescription: pageDescription, pageDescription: pageDescription,
pageData: pageData, pageData: pageData,
}); });
} }
} }

View File

@@ -1,31 +1,31 @@
import { ViewsPath } from '../Utils/Config'; import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const FeaturedResources: Array<ModelDocumentation> = const FeaturedResources: Array<ModelDocumentation> =
ResourceUtil.getFeaturedResources(); ResourceUtil.getFeaturedResources();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
let pageTitle: string = ''; let pageTitle: string = "";
let pageDescription: string = ''; let pageDescription: string = "";
const page: string | undefined = req.params['page']; const page: string | undefined = req.params["page"];
const pageData: any = {}; const pageData: any = {};
pageData.featuredResources = FeaturedResources; pageData.featuredResources = FeaturedResources;
pageTitle = 'Introduction'; pageTitle = "Introduction";
pageDescription = 'API Reference for OneUptime'; pageDescription = "API Reference for OneUptime";
return res.render(`${ViewsPath}/pages/index`, { return res.render(`${ViewsPath}/pages/index`, {
page: page, page: page,
resources: Resources, resources: Resources,
pageTitle: pageTitle, pageTitle: pageTitle,
pageDescription: pageDescription, pageDescription: pageDescription,
pageData: pageData, pageData: pageData,
}); });
} }
} }

View File

@@ -1,244 +1,238 @@
import { CodeExamplesPath, ViewsPath } from '../Utils/Config'; import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import PageNotFoundServiceHandler from './PageNotFound'; import PageNotFoundServiceHandler from "./PageNotFound";
import { AppApiRoute } from 'Common/ServiceRoute'; import { AppApiRoute } from "Common/ServiceRoute";
import { ColumnAccessControl } from 'Common/Types/BaseDatabase/AccessControl'; import { ColumnAccessControl } from "Common/Types/BaseDatabase/AccessControl";
import { getTableColumns } from 'Common/Types/Database/TableColumn'; import { getTableColumns } from "Common/Types/Database/TableColumn";
import Dictionary from 'Common/Types/Dictionary'; import Dictionary from "Common/Types/Dictionary";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import Permission, { import Permission, {
PermissionHelper, PermissionHelper,
PermissionProps, PermissionProps,
} from 'Common/Types/Permission'; } from "Common/Types/Permission";
import LocalCache from 'CommonServer/Infrastructure/LocalCache'; import LocalCache from "CommonServer/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
import LocalFile from 'CommonServer/Utils/LocalFile'; import LocalFile from "CommonServer/Utils/LocalFile";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const ResourceDictionary: Dictionary<ModelDocumentation> = const ResourceDictionary: Dictionary<ModelDocumentation> =
ResourceUtil.getResourceDictionaryByPath(); ResourceUtil.getResourceDictionaryByPath();
const PermissionDictionary: Dictionary<PermissionProps> = const PermissionDictionary: Dictionary<PermissionProps> =
PermissionHelper.getAllPermissionPropsAsDictionary(); PermissionHelper.getAllPermissionPropsAsDictionary();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
let pageTitle: string = ''; let pageTitle: string = "";
let pageDescription: string = ''; let pageDescription: string = "";
let page: string | undefined = req.params['page']; let page: string | undefined = req.params["page"];
const pageData: any = {}; const pageData: any = {};
if (!page) { if (!page) {
return PageNotFoundServiceHandler.executeResponse(req, res); return PageNotFoundServiceHandler.executeResponse(req, res);
}
const currentResource: ModelDocumentation | undefined =
ResourceDictionary[page];
if (!currentResource) {
return PageNotFoundServiceHandler.executeResponse(req, res);
}
// Resource Page.
pageTitle = currentResource.name;
pageDescription = currentResource.description;
page = 'model';
const tableColumns: any = getTableColumns(currentResource.model);
for (const key in tableColumns) {
const accessControl: ColumnAccessControl | null =
currentResource.model.getColumnAccessControlFor(key);
if (!accessControl) {
// remove columns with no access
delete tableColumns[key];
continue;
}
if (
accessControl?.create.length === 0 &&
accessControl?.read.length === 0 &&
accessControl?.update.length === 0
) {
// remove columns with no access
delete tableColumns[key];
continue;
}
tableColumns[key].permissions = accessControl;
}
delete tableColumns['deletedAt'];
delete tableColumns['deletedByUserId'];
delete tableColumns['deletedByUser'];
delete tableColumns['version'];
pageData.title = currentResource.model.singularName;
pageData.description = currentResource.model.tableDescription;
pageData.columns = tableColumns;
pageData.tablePermissions = {
read: currentResource.model.readRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
}
),
update: currentResource.model.updateRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
}
),
delete: currentResource.model.deleteRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
}
),
create: currentResource.model.createRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
}
),
};
pageData.listRequest = await LocalCache.getOrSetString(
'model',
'list-request',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/ListRequest.md`
);
}
);
pageData.itemRequest = await LocalCache.getOrSetString(
'model',
'item-request',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/ItemRequest.md`
);
}
);
pageData.itemResponse = await LocalCache.getOrSetString(
'model',
'item-response',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/ItemResponse.md`
);
}
);
pageData.countRequest = await LocalCache.getOrSetString(
'model',
'count-request',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/CountRequest.md`
);
}
);
pageData.countResponse = await LocalCache.getOrSetString(
'model',
'count-response',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/CountResponse.md`
);
}
);
pageData.updateRequest = await LocalCache.getOrSetString(
'model',
'update-request',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/UpdateRequest.md`
);
}
);
pageData.updateResponse = await LocalCache.getOrSetString(
'model',
'update-response',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/UpdateResponse.md`
);
}
);
pageData.createRequest = await LocalCache.getOrSetString(
'model',
'create-request',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/CreateRequest.md`
);
}
);
pageData.createResponse = await LocalCache.getOrSetString(
'model',
'create-response',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/CreateResponse.md`
);
}
);
pageData.deleteRequest = await LocalCache.getOrSetString(
'model',
'delete-request',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/DeleteRequest.md`
);
}
);
pageData.deleteResponse = await LocalCache.getOrSetString(
'model',
'delete-response',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/DeleteResponse.md`
);
}
);
pageData.listResponse = await LocalCache.getOrSetString(
'model',
'list-response',
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/ListResponse.md`
);
}
);
pageData.exampleObjectID = ObjectID.generate();
pageData.apiPath =
AppApiRoute.toString() +
currentResource.model.crudApiPath?.toString();
pageData.isMasterAdminApiDocs =
currentResource.model.isMasterAdminApiDocs;
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
pageTitle: pageTitle,
pageDescription: pageDescription,
pageData: pageData,
});
} }
const currentResource: ModelDocumentation | undefined =
ResourceDictionary[page];
if (!currentResource) {
return PageNotFoundServiceHandler.executeResponse(req, res);
}
// Resource Page.
pageTitle = currentResource.name;
pageDescription = currentResource.description;
page = "model";
const tableColumns: any = getTableColumns(currentResource.model);
for (const key in tableColumns) {
const accessControl: ColumnAccessControl | null =
currentResource.model.getColumnAccessControlFor(key);
if (!accessControl) {
// remove columns with no access
delete tableColumns[key];
continue;
}
if (
accessControl?.create.length === 0 &&
accessControl?.read.length === 0 &&
accessControl?.update.length === 0
) {
// remove columns with no access
delete tableColumns[key];
continue;
}
tableColumns[key].permissions = accessControl;
}
delete tableColumns["deletedAt"];
delete tableColumns["deletedByUserId"];
delete tableColumns["deletedByUser"];
delete tableColumns["version"];
pageData.title = currentResource.model.singularName;
pageData.description = currentResource.model.tableDescription;
pageData.columns = tableColumns;
pageData.tablePermissions = {
read: currentResource.model.readRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
update: currentResource.model.updateRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
delete: currentResource.model.deleteRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
create: currentResource.model.createRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
};
pageData.listRequest = await LocalCache.getOrSetString(
"model",
"list-request",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/Model/ListRequest.md`);
},
);
pageData.itemRequest = await LocalCache.getOrSetString(
"model",
"item-request",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/Model/ItemRequest.md`);
},
);
pageData.itemResponse = await LocalCache.getOrSetString(
"model",
"item-response",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/ItemResponse.md`,
);
},
);
pageData.countRequest = await LocalCache.getOrSetString(
"model",
"count-request",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/CountRequest.md`,
);
},
);
pageData.countResponse = await LocalCache.getOrSetString(
"model",
"count-response",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/CountResponse.md`,
);
},
);
pageData.updateRequest = await LocalCache.getOrSetString(
"model",
"update-request",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/UpdateRequest.md`,
);
},
);
pageData.updateResponse = await LocalCache.getOrSetString(
"model",
"update-response",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/UpdateResponse.md`,
);
},
);
pageData.createRequest = await LocalCache.getOrSetString(
"model",
"create-request",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/CreateRequest.md`,
);
},
);
pageData.createResponse = await LocalCache.getOrSetString(
"model",
"create-response",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/CreateResponse.md`,
);
},
);
pageData.deleteRequest = await LocalCache.getOrSetString(
"model",
"delete-request",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/DeleteRequest.md`,
);
},
);
pageData.deleteResponse = await LocalCache.getOrSetString(
"model",
"delete-response",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/DeleteResponse.md`,
);
},
);
pageData.listResponse = await LocalCache.getOrSetString(
"model",
"list-response",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/Model/ListResponse.md`,
);
},
);
pageData.exampleObjectID = ObjectID.generate();
pageData.apiPath =
AppApiRoute.toString() + currentResource.model.crudApiPath?.toString();
pageData.isMasterAdminApiDocs = currentResource.model.isMasterAdminApiDocs;
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
pageTitle: pageTitle,
pageDescription: pageDescription,
pageData: pageData,
});
}
} }

View File

@@ -1,21 +1,21 @@
import { ViewsPath } from '../Utils/Config'; import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
_req: ExpressRequest, _req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
res.status(404); res.status(404);
return res.render(`${ViewsPath}/pages/index`, { return res.render(`${ViewsPath}/pages/index`, {
page: '404', page: "404",
pageTitle: 'Page Not Found', pageTitle: "Page Not Found",
pageDescription: "Page you're looking for is not found.", pageDescription: "Page you're looking for is not found.",
resources: Resources, resources: Resources,
pageData: {}, pageData: {},
}); });
} }
} }

View File

@@ -1,50 +1,50 @@
import { CodeExamplesPath, ViewsPath } from '../Utils/Config'; import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import LocalCache from 'CommonServer/Infrastructure/LocalCache'; import LocalCache from "CommonServer/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
import LocalFile from 'CommonServer/Utils/LocalFile'; import LocalFile from "CommonServer/Utils/LocalFile";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
let pageTitle: string = ''; let pageTitle: string = "";
let pageDescription: string = ''; let pageDescription: string = "";
const page: string | undefined = req.params['page']; const page: string | undefined = req.params["page"];
const pageData: any = {}; const pageData: any = {};
pageTitle = 'Pagination'; pageTitle = "Pagination";
pageDescription = 'Learn how to paginate requests with OneUptime API'; pageDescription = "Learn how to paginate requests with OneUptime API";
pageData.responseCode = await LocalCache.getOrSetString( pageData.responseCode = await LocalCache.getOrSetString(
'pagination', "pagination",
'response', "response",
async () => { async () => {
return await LocalFile.read( return await LocalFile.read(
`${CodeExamplesPath}/Pagination/Response.md` `${CodeExamplesPath}/Pagination/Response.md`,
);
}
); );
},
);
pageData.requestCode = await LocalCache.getOrSetString( pageData.requestCode = await LocalCache.getOrSetString(
'pagination', "pagination",
'request', "request",
async () => { async () => {
return await LocalFile.read( return await LocalFile.read(
`${CodeExamplesPath}/Pagination/Request.md` `${CodeExamplesPath}/Pagination/Request.md`,
);
}
); );
},
);
return res.render(`${ViewsPath}/pages/index`, { return res.render(`${ViewsPath}/pages/index`, {
page: page, page: page,
resources: Resources, resources: Resources,
pageTitle: pageTitle, pageTitle: pageTitle,
pageDescription: pageDescription, pageDescription: pageDescription,
pageData: pageData, pageData: pageData,
}); });
} }
} }

View File

@@ -1,35 +1,35 @@
import { ViewsPath } from '../Utils/Config'; import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { PermissionHelper, PermissionProps } from 'Common/Types/Permission'; import { PermissionHelper, PermissionProps } from "Common/Types/Permission";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
let pageTitle: string = ''; let pageTitle: string = "";
let pageDescription: string = ''; let pageDescription: string = "";
const page: string | undefined = req.params['page']; const page: string | undefined = req.params["page"];
const pageData: any = {}; const pageData: any = {};
pageTitle = 'Permissions'; pageTitle = "Permissions";
pageDescription = 'Learn how permissions work with OneUptime'; pageDescription = "Learn how permissions work with OneUptime";
pageData.permissions = PermissionHelper.getAllPermissionProps().filter( pageData.permissions = PermissionHelper.getAllPermissionProps().filter(
(i: PermissionProps) => { (i: PermissionProps) => {
return i.isAssignableToTenant; return i.isAssignableToTenant;
} },
); );
return res.render(`${ViewsPath}/pages/index`, { return res.render(`${ViewsPath}/pages/index`, {
page: page, page: page,
resources: Resources, resources: Resources,
pageTitle: pageTitle, pageTitle: pageTitle,
pageDescription: pageDescription, pageDescription: pageDescription,
pageData: pageData, pageData: pageData,
}); });
} }
} }

View File

@@ -1,21 +1,21 @@
import { ViewsPath } from '../Utils/Config'; import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from '../Utils/Resources'; import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressRequest, ExpressResponse } from "CommonServer/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler { export default class ServiceHandler {
public static async executeResponse( public static async executeResponse(
_req: ExpressRequest, _req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> { ): Promise<void> {
res.status(200); res.status(200);
return res.render(`${ViewsPath}/pages/index`, { return res.render(`${ViewsPath}/pages/index`, {
page: 'status', page: "status",
pageTitle: 'Status', pageTitle: "Status",
pageDescription: '200 - Success', pageDescription: "200 - Success",
resources: Resources, resources: Resources,
pageData: {}, pageData: {},
}); });
} }
} }

View File

@@ -1,4 +1,4 @@
export const ViewsPath: string = '/usr/src/app/FeatureSet/ApiReference/views'; export const ViewsPath: string = "/usr/src/app/FeatureSet/ApiReference/views";
export const StaticPath: string = '/usr/src/app/FeatureSet/ApiReference/Static'; export const StaticPath: string = "/usr/src/app/FeatureSet/ApiReference/Static";
export const CodeExamplesPath: string = export const CodeExamplesPath: string =
'/usr/src/app/FeatureSet/ApiReference/CodeExamples'; "/usr/src/app/FeatureSet/ApiReference/CodeExamples";

View File

@@ -1,74 +1,73 @@
import BaseModel from 'Common/Models/BaseModel'; import BaseModel from "Common/Models/BaseModel";
import ArrayUtil from 'Common/Types/ArrayUtil'; import ArrayUtil from "Common/Types/ArrayUtil";
import Dictionary from 'Common/Types/Dictionary'; import Dictionary from "Common/Types/Dictionary";
import { IsBillingEnabled } from 'CommonServer/EnvironmentConfig'; import { IsBillingEnabled } from "CommonServer/EnvironmentConfig";
import Models from 'Model/Models/Index'; import Models from "Model/Models/Index";
export interface ModelDocumentation { export interface ModelDocumentation {
name: string; name: string;
path: string; path: string;
model: BaseModel; model: BaseModel;
description: string; description: string;
} }
export default class ResourceUtil { export default class ResourceUtil {
public static getResources(): Array<ModelDocumentation> { public static getResources(): Array<ModelDocumentation> {
const resources: Array<ModelDocumentation> = Models.filter( const resources: Array<ModelDocumentation> = Models.filter(
(model: typeof BaseModel) => { (model: typeof BaseModel) => {
const modelInstance: BaseModel = new model(); const modelInstance: BaseModel = new model();
let showDocs: boolean = modelInstance.enableDocumentation; let showDocs: boolean = modelInstance.enableDocumentation;
if (modelInstance.isMasterAdminApiDocs && IsBillingEnabled) { if (modelInstance.isMasterAdminApiDocs && IsBillingEnabled) {
showDocs = false; showDocs = false;
}
return showDocs;
}
)
.map((model: typeof BaseModel) => {
const modelInstance: BaseModel = new model();
return {
name: modelInstance.singularName!,
path: modelInstance.getAPIDocumentationPath(),
model: modelInstance,
description: modelInstance.tableDescription!,
};
})
.sort(ArrayUtil.sortByFieldName('name'));
return resources;
}
public static getFeaturedResources(): Array<ModelDocumentation> {
const featuredResources: Array<string> = [
'Monitor',
'Scheduled Maintenance Event',
'Status Page',
'Incident',
'Team',
'On-Call Duty',
'Label',
'Team Member',
];
return ResourceUtil.getResources().filter(
(resource: ModelDocumentation) => {
return featuredResources.includes(resource.name);
}
);
}
public static getResourceDictionaryByPath(): Dictionary<ModelDocumentation> {
const dict: Dictionary<ModelDocumentation> = {};
const resources: Array<ModelDocumentation> =
ResourceUtil.getResources();
for (const resource of resources) {
dict[resource.path] = resource;
} }
return dict; return showDocs;
},
)
.map((model: typeof BaseModel) => {
const modelInstance: BaseModel = new model();
return {
name: modelInstance.singularName!,
path: modelInstance.getAPIDocumentationPath(),
model: modelInstance,
description: modelInstance.tableDescription!,
};
})
.sort(ArrayUtil.sortByFieldName("name"));
return resources;
}
public static getFeaturedResources(): Array<ModelDocumentation> {
const featuredResources: Array<string> = [
"Monitor",
"Scheduled Maintenance Event",
"Status Page",
"Incident",
"Team",
"On-Call Duty",
"Label",
"Team Member",
];
return ResourceUtil.getResources().filter(
(resource: ModelDocumentation) => {
return featuredResources.includes(resource.name);
},
);
}
public static getResourceDictionaryByPath(): Dictionary<ModelDocumentation> {
const dict: Dictionary<ModelDocumentation> = {};
const resources: Array<ModelDocumentation> = ResourceUtil.getResources();
for (const resource of resources) {
dict[resource.path] = resource;
} }
return dict;
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,93 +1,85 @@
import { ContentPath, StaticPath, ViewsPath } from './Utils/Config'; import { ContentPath, StaticPath, ViewsPath } from "./Utils/Config";
import DocsNav, { NavGroup, NavLink } from './Utils/Nav'; import DocsNav, { NavGroup, NavLink } from "./Utils/Nav";
import DocsRender from './Utils/Render'; import DocsRender from "./Utils/Render";
import FeatureSet from 'CommonServer/Types/FeatureSet'; import FeatureSet from "CommonServer/Types/FeatureSet";
import Express, { import Express, {
ExpressApplication, ExpressApplication,
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressStatic, ExpressStatic,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import LocalFile from 'CommonServer/Utils/LocalFile'; import LocalFile from "CommonServer/Utils/LocalFile";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import 'ejs'; import "ejs";
const DocsFeatureSet: FeatureSet = { const DocsFeatureSet: FeatureSet = {
init: async (): Promise<void> => { init: async (): Promise<void> => {
const app: ExpressApplication = Express.getExpressApp(); const app: ExpressApplication = Express.getExpressApp();
app.get('/docs', (_req: ExpressRequest, res: ExpressResponse) => { app.get("/docs", (_req: ExpressRequest, res: ExpressResponse) => {
res.redirect('/docs/introduction/getting-started'); res.redirect("/docs/introduction/getting-started");
}); });
app.get( app.get(
'/docs/:categorypath/:pagepath', "/docs/:categorypath/:pagepath",
async (_req: ExpressRequest, res: ExpressResponse) => { async (_req: ExpressRequest, res: ExpressResponse) => {
try { try {
const fullPath: string = const fullPath: string =
`${_req.params['categorypath']}/${_req.params['pagepath']}`.toLowerCase(); `${_req.params["categorypath"]}/${_req.params["pagepath"]}`.toLowerCase();
// read file from Content folder. // read file from Content folder.
let contentInMarkdown: string = await LocalFile.read( let contentInMarkdown: string = await LocalFile.read(
`${ContentPath}/${fullPath}.md` `${ContentPath}/${fullPath}.md`,
); );
// remove first line from content because we dont want to show title in content. Title is already in nav. // remove first line from content because we dont want to show title in content. Title is already in nav.
contentInMarkdown = contentInMarkdown contentInMarkdown = contentInMarkdown.split("\n").slice(1).join("\n");
.split('\n')
.slice(1)
.join('\n');
const renderedContent: string = await DocsRender.render( const renderedContent: string =
contentInMarkdown await DocsRender.render(contentInMarkdown);
);
const currentCategory: NavGroup | undefined = DocsNav.find( const currentCategory: NavGroup | undefined = DocsNav.find(
(category: NavGroup) => { (category: NavGroup) => {
return category.links.find((link: NavLink) => { return category.links.find((link: NavLink) => {
return link.url return link.url.toLocaleLowerCase().includes(fullPath);
.toLocaleLowerCase() });
.includes(fullPath); },
}); );
}
);
const currrentNavLink: NavLink | undefined = const currrentNavLink: NavLink | undefined =
currentCategory?.links.find((link: NavLink) => { currentCategory?.links.find((link: NavLink) => {
return link.url return link.url.toLocaleLowerCase().includes(fullPath);
.toLocaleLowerCase() });
.includes(fullPath);
});
if (!currentCategory || !currrentNavLink) { if (!currentCategory || !currrentNavLink) {
// render not found. // render not found.
res.status(404); res.status(404);
return res.render(`${ViewsPath}/NotFound`, { return res.render(`${ViewsPath}/NotFound`, {
nav: DocsNav, nav: DocsNav,
}); });
} }
res.render(`${ViewsPath}/Index`, { res.render(`${ViewsPath}/Index`, {
nav: DocsNav, nav: DocsNav,
content: renderedContent, content: renderedContent,
category: currentCategory, category: currentCategory,
link: currrentNavLink, link: currrentNavLink,
githubPath: fullPath, githubPath: fullPath,
}); });
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
res.status(500); res.status(500);
return res.render(`${ViewsPath}/ServerError`, { return res.render(`${ViewsPath}/ServerError`, {
nav: DocsNav, nav: DocsNav,
}); });
} }
} },
); );
app.use('/docs/static', ExpressStatic(StaticPath)); app.use("/docs/static", ExpressStatic(StaticPath));
}, },
}; };
export default DocsFeatureSet; export default DocsFeatureSet;

View File

@@ -1,3 +1,3 @@
export const ViewsPath: string = '/usr/src/app/FeatureSet/Docs/Views'; export const ViewsPath: string = "/usr/src/app/FeatureSet/Docs/Views";
export const StaticPath: string = '/usr/src/app/FeatureSet/Docs/Static'; export const StaticPath: string = "/usr/src/app/FeatureSet/Docs/Static";
export const ContentPath: string = '/usr/src/app/FeatureSet/Docs/Content'; export const ContentPath: string = "/usr/src/app/FeatureSet/Docs/Content";

View File

@@ -1,75 +1,75 @@
export interface NavLink { export interface NavLink {
title: string; title: string;
url: string; url: string;
} }
export interface NavGroup { export interface NavGroup {
title: string; title: string;
links: NavLink[]; links: NavLink[];
} }
const DocsNav: NavGroup[] = [ const DocsNav: NavGroup[] = [
{ {
title: 'Introduction', title: "Introduction",
links: [ links: [
{ {
title: 'Getting Started', title: "Getting Started",
url: '/docs/introduction/getting-started', url: "/docs/introduction/getting-started",
}, },
], ],
}, },
{ {
title: 'Installation', title: "Installation",
links: [ links: [
{ {
title: 'Local Development', title: "Local Development",
url: '/docs/installation/local-development', url: "/docs/installation/local-development",
}, },
{ {
title: 'Docker Compose', title: "Docker Compose",
url: '/docs/installation/docker-compose', url: "/docs/installation/docker-compose",
}, },
{ {
title: 'Kubernetes and Helm', title: "Kubernetes and Helm",
url: 'https://artifacthub.io/packages/helm/oneuptime/oneuptime', url: "https://artifacthub.io/packages/helm/oneuptime/oneuptime",
}, },
], ],
}, },
{ {
title: 'Monitor', title: "Monitor",
links: [ links: [
{ {
title: 'Custom Code Monitor', title: "Custom Code Monitor",
url: '/docs/monitor/custom-code-monitor', url: "/docs/monitor/custom-code-monitor",
}, },
{ {
title: 'Synthetic Monitor', title: "Synthetic Monitor",
url: '/docs/monitor/synthetic-monitor', url: "/docs/monitor/synthetic-monitor",
}, },
{ {
title: 'JavaScript Expressions', title: "JavaScript Expressions",
url: '/docs/monitor/javascript-expression', url: "/docs/monitor/javascript-expression",
}, },
{ {
title: 'Monitor Secrets', title: "Monitor Secrets",
url: '/docs/monitor/monitor-secrets', url: "/docs/monitor/monitor-secrets",
}, },
], ],
}, },
{ {
title: 'Probe', title: "Probe",
links: [ links: [
{ title: 'Custom Probes', url: '/docs/probe/custom-probe' }, { title: "Custom Probes", url: "/docs/probe/custom-probe" },
{ title: 'IP Addresses', url: '/docs/probe/ip-address' }, { title: "IP Addresses", url: "/docs/probe/ip-address" },
], ],
}, },
{ {
title: 'Telemetry', title: "Telemetry",
links: [ links: [
{ title: 'OpenTelemetry', url: '/docs/telemetry/open-telemetry' }, { title: "OpenTelemetry", url: "/docs/telemetry/open-telemetry" },
{ title: 'Fluentd', url: '/docs/telemetry/fluentd' }, { title: "Fluentd", url: "/docs/telemetry/fluentd" },
], ],
}, },
]; ];
export default DocsNav; export default DocsNav;

View File

@@ -1,10 +1,7 @@
import Markdown, { MarkdownContentType } from 'CommonServer/Types/Markdown'; import Markdown, { MarkdownContentType } from "CommonServer/Types/Markdown";
export default class DocsRender { export default class DocsRender {
public static async render(markdownContent: string): Promise<string> { public static async render(markdownContent: string): Promise<string> {
return Markdown.convertToHTML( return Markdown.convertToHTML(markdownContent, MarkdownContentType.Docs);
markdownContent, }
MarkdownContentType.Docs
);
}
} }

View File

@@ -1,89 +1,88 @@
import BlogPostUtil, { BlogPost, BlogPostHeader } from '../Utils/BlogPost'; import BlogPostUtil, { BlogPost, BlogPostHeader } from "../Utils/BlogPost";
import { ViewsPath } from '../Utils/Config'; import { ViewsPath } from "../Utils/Config";
import NotFoundUtil from '../Utils/NotFound'; import NotFoundUtil from "../Utils/NotFound";
import ServerErrorUtil from '../Utils/ServerError'; import ServerErrorUtil from "../Utils/ServerError";
import Text from 'Common/Types/Text'; import Text from "Common/Types/Text";
import Express, { import Express, {
ExpressApplication, ExpressApplication,
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
const app: ExpressApplication = Express.getExpressApp(); const app: ExpressApplication = Express.getExpressApp();
app.get( app.get(
'/blog/post/:file', "/blog/post/:file",
async (req: ExpressRequest, res: ExpressResponse) => { async (req: ExpressRequest, res: ExpressResponse) => {
try { try {
const fileName: string = req.params['file'] as string; const fileName: string = req.params["file"] as string;
const blogPost: BlogPost | null = await BlogPostUtil.getBlogPost( const blogPost: BlogPost | null =
fileName await BlogPostUtil.getBlogPost(fileName);
);
if (!blogPost) { if (!blogPost) {
return NotFoundUtil.renderNotFound(res); return NotFoundUtil.renderNotFound(res);
} }
res.render(`${ViewsPath}/Blog/Post`, { res.render(`${ViewsPath}/Blog/Post`, {
support: false, support: false,
footerCards: true, footerCards: true,
cta: true, cta: true,
blackLogo: false, blackLogo: false,
requestDemoCta: false, requestDemoCta: false,
blogPost: blogPost, blogPost: blogPost,
}); });
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
return ServerErrorUtil.renderServerError(res); return ServerErrorUtil.renderServerError(res);
}
} }
},
); );
// List all blog posts with tag // List all blog posts with tag
app.get( app.get(
'/blog/tag/:tagName', "/blog/tag/:tagName",
async (req: ExpressRequest, res: ExpressResponse) => { async (req: ExpressRequest, res: ExpressResponse) => {
try { try {
const tagName: string = req.params['tagName'] as string; const tagName: string = req.params["tagName"] as string;
const blogPosts: Array<BlogPostHeader> = const blogPosts: Array<BlogPostHeader> =
await BlogPostUtil.getBlogPostList(tagName); await BlogPostUtil.getBlogPostList(tagName);
res.render(`${ViewsPath}/Blog/ListByTag`, { res.render(`${ViewsPath}/Blog/ListByTag`, {
support: false, support: false,
footerCards: true, footerCards: true,
cta: true, cta: true,
blackLogo: false, blackLogo: false,
requestDemoCta: false, requestDemoCta: false,
blogPosts: blogPosts, blogPosts: blogPosts,
tagName: Text.fromDashesToPascalCase(tagName), tagName: Text.fromDashesToPascalCase(tagName),
}); });
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
return ServerErrorUtil.renderServerError(res); return ServerErrorUtil.renderServerError(res);
}
} }
},
); );
// main blog page // main blog page
app.get('/blog', async (_req: ExpressRequest, res: ExpressResponse) => { app.get("/blog", async (_req: ExpressRequest, res: ExpressResponse) => {
try { try {
const blogPosts: Array<BlogPostHeader> = const blogPosts: Array<BlogPostHeader> =
await BlogPostUtil.getBlogPostList(); await BlogPostUtil.getBlogPostList();
res.render(`${ViewsPath}/Blog/List`, { res.render(`${ViewsPath}/Blog/List`, {
support: false, support: false,
footerCards: true, footerCards: true,
cta: true, cta: true,
blackLogo: false, blackLogo: false,
requestDemoCta: false, requestDemoCta: false,
blogPosts: blogPosts, blogPosts: blogPosts,
}); });
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
return ServerErrorUtil.renderServerError(res); return ServerErrorUtil.renderServerError(res);
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,99 @@
(function (e, t) { (function (e, t) {
var n = e.amplitude || { _q: [], _iq: {} }; var r = t.createElement("script") var n = e.amplitude || { _q: [], _iq: {} };
; r.type = "text/javascript" var r = t.createElement("script");
; r.integrity = "sha384-vYYnQ3LPdp/RkQjoKBTGSq0X5F73gXU3G2QopHaIfna0Ct1JRWzwrmEz115NzOta" r.type = "text/javascript";
; r.crossOrigin = "anonymous"; r.async = true r.integrity =
; r.src = "https://cdn.amplitude.com/libs/amplitude-5.8.0-min.gz.js" "sha384-vYYnQ3LPdp/RkQjoKBTGSq0X5F73gXU3G2QopHaIfna0Ct1JRWzwrmEz115NzOta";
; r.onload = function () { r.crossOrigin = "anonymous";
if (!e.amplitude.runQueuedFunctions) { r.async = true;
console.log("[Amplitude] Error: could not load SDK") r.src = "https://cdn.amplitude.com/libs/amplitude-5.8.0-min.gz.js";
} r.onload = function () {
} if (!e.amplitude.runQueuedFunctions) {
; var i = t.getElementsByTagName("script")[0]; i.parentNode.insertBefore(r, i) console.log("[Amplitude] Error: could not load SDK");
; function s(e, t) { }
e.prototype[t] = function () { };
this._q.push([t].concat(Array.prototype.slice.call(arguments, 0))); return this var i = t.getElementsByTagName("script")[0];
} i.parentNode.insertBefore(r, i);
} function s(e, t) {
var o = function () { this._q = []; return this } e.prototype[t] = function () {
; var a = ["add", "append", "clearAll", "prepend", "set", "setOnce", "unset"] this._q.push([t].concat(Array.prototype.slice.call(arguments, 0)));
; for (var u = 0; u < a.length; u++) { s(o, a[u]) } n.Identify = o; var c = function () { return this;
this._q = [] };
; return this }
} var o = function () {
; var l = ["setProductId", "setQuantity", "setPrice", "setRevenueType", "setEventProperties"] this._q = [];
; for (var p = 0; p < l.length; p++) { s(c, l[p]) } n.Revenue = c return this;
; var d = ["init", "logEvent", "logRevenue", "setUserId", "setUserProperties", "setOptOut", "setVersionName", "setDomain", "setDeviceId", "enableTracking", "setGlobalUserProperties", "identify", "clearUserProperties", "setGroup", "logRevenueV2", "regenerateDeviceId", "groupIdentify", "onInit", "logEventWithTimestamp", "logEventWithGroups", "setSessionId", "resetSessionId"] };
; function v(e) { var a = ["add", "append", "clearAll", "prepend", "set", "setOnce", "unset"];
function t(t) { for (var u = 0; u < a.length; u++) {
e[t] = function () { s(o, a[u]);
e._q.push([t].concat(Array.prototype.slice.call(arguments, 0))) }
} n.Identify = o;
} var c = function () {
for (var n = 0; n < d.length; n++) { t(d[n]) } this._q = [];
} v(n); n.getInstance = function (e) { return this;
e = (!e || e.length === 0 ? "$default_instance" : e).toLowerCase() };
; if (!n._iq.hasOwnProperty(e)) { n._iq[e] = { _q: [] }; v(n._iq[e]) } return n._iq[e] var l = [
} "setProductId",
; e.amplitude = n "setQuantity",
"setPrice",
"setRevenueType",
"setEventProperties",
];
for (var p = 0; p < l.length; p++) {
s(c, l[p]);
}
n.Revenue = c;
var d = [
"init",
"logEvent",
"logRevenue",
"setUserId",
"setUserProperties",
"setOptOut",
"setVersionName",
"setDomain",
"setDeviceId",
"enableTracking",
"setGlobalUserProperties",
"identify",
"clearUserProperties",
"setGroup",
"logRevenueV2",
"regenerateDeviceId",
"groupIdentify",
"onInit",
"logEventWithTimestamp",
"logEventWithGroups",
"setSessionId",
"resetSessionId",
];
function v(e) {
function t(t) {
e[t] = function () {
e._q.push([t].concat(Array.prototype.slice.call(arguments, 0)));
};
}
for (var n = 0; n < d.length; n++) {
t(d[n]);
}
}
v(n);
n.getInstance = function (e) {
e = (!e || e.length === 0 ? "$default_instance" : e).toLowerCase();
if (!n._iq.hasOwnProperty(e)) {
n._iq[e] = { _q: [] };
v(n._iq[e]);
}
return n._iq[e];
};
e.amplitude = n;
})(window, document); })(window, document);
amplitude.getInstance().init("802d95003af23aad17ed068b6cfdeb2b", null, { amplitude.getInstance().init("802d95003af23aad17ed068b6cfdeb2b", null, {
// include referrer information in amplitude. // include referrer information in amplitude.
saveEvents: true, saveEvents: true,
includeUtm: true, includeUtm: true,
includeReferrer: true, includeReferrer: true,
includeGclid: true includeGclid: true,
}); });

View File

@@ -1,94 +1,95 @@
function openTab(evt, tabName) { function openTab(evt, tabName) {
// Declare all variables // Declare all variables
let i; let i;
// Get all elements with class="tabcontent" and hide them // Get all elements with class="tabcontent" and hide them
const tabcontent = document.getElementsByClassName('tabcontent'); const tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) { for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].className = tabcontent[i].className.replace(' active', ''); tabcontent[i].className = tabcontent[i].className.replace(" active", "");
} }
// Get all elements with class="tablinks" and remove the class "active" // Get all elements with class="tablinks" and remove the class "active"
const tablinks = document.getElementsByClassName('tablinks'); const tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) { for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(' active', ''); tablinks[i].className = tablinks[i].className.replace(" active", "");
} }
// Show the current tab, and add an "active" class to the link that opened the tab // Show the current tab, and add an "active" class to the link that opened the tab
document.getElementById(tabName).className += ' active'; document.getElementById(tabName).className += " active";
evt.currentTarget.className += ' active'; evt.currentTarget.className += " active";
setTimeout(() => document.getElementById(tabName + '1').parentNode.click(), 200); setTimeout(
() => document.getElementById(tabName + "1").parentNode.click(),
200,
);
} }
function openTooltip(name) { function openTooltip(name) {
// Declare all variables // Declare all variables
let i; let i;
const element = document.getElementById(name); const element = document.getElementById(name);
const elclass = element.className; const elclass = element.className;
const tooltip = document.getElementsByClassName('tooltiptext'); const tooltip = document.getElementsByClassName("tooltiptext");
for (i = 0; i < tooltip.length; i++) { for (i = 0; i < tooltip.length; i++) {
tooltip[i].className = tooltip[i].className.replace(' active', ''); tooltip[i].className = tooltip[i].className.replace(" active", "");
} }
if (elclass.indexOf('active') > -1) { if (elclass.indexOf("active") > -1) {
element.className = element.className.replace(" active", "");
element.className = element.className.replace(' active', ''); } else {
} element.classList.add("active");
else { }
element.classList.add('active');
}
} }
window.onload = function () { window.onload = function () {
animateHTML().init(); animateHTML().init();
const tooltext = document.getElementsByClassName('tooltiptext'); const tooltext = document.getElementsByClassName("tooltiptext");
for (let i = 0; i < tooltext.length; i++) { for (let i = 0; i < tooltext.length; i++) {
tooltext[i].onclick = function (e) {
e.stopPropagation();
};
}
tooltext[i].onclick = function (e) { document.getElementsByTagName("body")[0].onclick = function (e) {
e.stopPropagation(); if (
} e.target.className !== "popover-dot" &&
e.target.className !== "tooltiptext" &&
e.target.className !== "tablinks active"
) {
const tooltip = document.getElementsByClassName("tooltiptext");
for (let i = 0; i < tooltip.length; i++) {
tooltip[i].className = tooltip[i].className.replace(" active", "");
}
} }
};
document.getElementsByTagName('body')[0].onclick = function (e) { };
if (e.target.className !== 'popover-dot' && e.target.className !== 'tooltiptext' && e.target.className !== 'tablinks active') {
const tooltip = document.getElementsByClassName('tooltiptext');
for (let i = 0; i < tooltip.length; i++) {
tooltip[i].className = tooltip[i].className.replace(' active', '');
}
}
}
}
const animateHTML = function () { const animateHTML = function () {
let elem, windowHeight; let elem, windowHeight;
const init = function () { const init = function () {
elem = document.getElementById('Statuspage'); elem = document.getElementById("Statuspage");
windowHeight = window.innerHeight; windowHeight = window.innerHeight;
_addEventHandlers(); _addEventHandlers();
};
const _addEventHandlers = function () {
window.addEventListener("scroll", _checkPosition);
window.addEventListener("resize", init);
};
const _checkPosition = function () {
if (!elem) {
return;
} }
const _addEventHandlers = function () { const posFromTop = elem.getBoundingClientRect().top;
window.addEventListener('scroll', _checkPosition)
window.addEventListener('resize', init)
}
const _checkPosition = function () {
if (!elem) {
return;
}
const posFromTop = elem.getBoundingClientRect().top;
if (posFromTop - windowHeight <= -400) { if (posFromTop - windowHeight <= -400) {
document.getElementById("Statuspage1").parentNode.click();
document.getElementById('Statuspage1').parentNode.click(); window.removeEventListener("scroll", _checkPosition);
window.removeEventListener('scroll', _checkPosition); window.removeEventListener("resize", init);
window.removeEventListener('resize', init); return;
return;
}
} }
return { };
init: init return {
} init: init,
} };
};

View File

@@ -1,5 +1,5 @@
// This is basicaly meant to get a cookie by name // This is basicaly meant to get a cookie by name
var getCookiebyName = function (name) { var getCookiebyName = function (name) {
var pair = document.cookie.match(new RegExp(name + '=([^;]+)')); var pair = document.cookie.match(new RegExp(name + "=([^;]+)"));
return pair ? pair[1] : null; return pair ? pair[1] : null;
}; };

View File

@@ -1,69 +1,49 @@
!(function () {
function n(n, e) {
$(".hidden", n)
.eq(e)
.css({
transitionDelay: Math.random() + Math.random() + "s",
transitionDuration: 2 * Math.random() + 0.2 + "s",
}),
$(".hidden", n).eq(e).attr("class", "shown");
}
! function () { function e(n, e) {
function n(n, e) { if (n.hasClass("is-visible")) {
const a = $(".shown", n).eq(e);
$('.hidden', n) a.attr("class", "hidden"),
.eq(e) setTimeout(function () {
.css({ a.attr("class", "shown");
transitionDelay: Math.random() + Math.random() + 's', }, 3e3);
transitionDuration: 2 * Math.random() + .2 + 's'
}), $('.hidden', n)
.eq(e)
.attr('class', 'shown')
} }
}
function e(n, e) {
if (n.hasClass('is-visible')) { $(".card").each(function (e, a) {
if (window.IntersectionObserver)
const a = $('.shown', n) (a.observer = new IntersectionObserver((e) => {
.eq(e); e.forEach((e) => {
a.attr('class', 'hidden'), setTimeout(function () { if (e.isIntersecting || e.intersectionRatio > 0) {
a.attr('class', 'shown') $(a).addClass("is-visible");
}, 3e3)
} for (let t = $(".hidden", a).length; t >= 0; t--) n(a, t);
} else $(a).removeClass("is-visible");
});
})),
a.observer.observe(a);
else {
$(a).addClass("is-visible");
for (let t = $(".hidden", a).length; t >= 0; t--) n(a, t);
} }
}),
$('.card') setInterval(function () {
.each(function (e, a) { let n = $(".card").eq(Math.floor(Math.random() * $(".card").length));
if (window.IntersectionObserver) a.observer = new IntersectionObserver(e => {
e.forEach(e => { e(n, Math.floor(Math.random() * $(".shown", n).length));
if (e.isIntersecting || e.intersectionRatio > 0) {
n = $(".card").eq(Math.floor(Math.random() * $(".card").length));
$(a)
.addClass('is-visible'); e(n, Math.floor(Math.random() * $(".shown", n).length));
}, 600);
for (let t = $('.hidden', a) })();
.length; t >= 0; t--) n(a, t)
} else $(a)
.removeClass('is-visible')
})
}), a.observer.observe(a);
else {
$(a)
.addClass('is-visible');
for (let t = $('.hidden', a)
.length; t >= 0; t--) n(a, t)
}
}), setInterval(function () {
let n = $('.card')
.eq(Math.floor(Math.random() * $('.card')
.length));
e(n, Math.floor(Math.random() * $('.shown', n)
.length));
n = $('.card')
.eq(Math.floor(Math.random() * $('.card')
.length));
e(n, Math.floor(Math.random() * $('.shown', n)
.length))
}, 600)
}();

View File

@@ -1,27 +1,26 @@
let accountsUrl = window.location.origin + "/accounts";
let accountsUrl = window.location.origin+'/accounts'; let backendUrl =
let backendUrl = window.location.hostname==='localhost'? 'http://localhost:3002': window.location.origin+'/api' window.location.hostname === "localhost"
? "http://localhost:3002"
: window.location.origin + "/api";
//eslint-disable-next-line //eslint-disable-next-line
function loginUrl(extra) { function loginUrl(extra) {
if (extra) { if (extra) {
window.location.href = `${accountsUrl}/login${extra}`; window.location.href = `${accountsUrl}/login${extra}`;
} } else {
else { window.location.href = `${accountsUrl}/login`;
window.location.href = `${accountsUrl}/login`; }
}
} }
//eslint-disable-next-line //eslint-disable-next-line
function registerUrl(params) { function registerUrl(params) {
if (params) { if (params) {
window.location.href = `${accountsUrl}/register${params}`; window.location.href = `${accountsUrl}/register${params}`;
} } else {
else { window.location.href = `${accountsUrl}/register`;
window.location.href = `${accountsUrl}/register`; }
}
} }
//eslint-disable-next-line //eslint-disable-next-line
function formUrl() { function formUrl() {
return `${backendUrl}/lead/`; return `${backendUrl}/lead/`;
} }

View File

@@ -1,3 +1,3 @@
test('two plus two is four', () => { test("two plus two is four", () => {
expect(2 + 2).toBe(4); expect(2 + 2).toBe(4);
}); });

View File

@@ -1,453 +1,447 @@
import AnalyticsBaseModel from 'Common/AnalyticsModels/BaseModel'; import AnalyticsBaseModel from "Common/AnalyticsModels/BaseModel";
import BaseModel from 'Common/Models/BaseModel'; import BaseModel from "Common/Models/BaseModel";
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse'; import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from 'Common/Types/API/HTTPResponse'; import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONArray, JSONObject, JSONObjectOrArray } from 'Common/Types/JSON'; import { JSONArray, JSONObject, JSONObjectOrArray } from "Common/Types/JSON";
import JSONFunctions from 'Common/Types/JSONFunctions'; import JSONFunctions from "Common/Types/JSONFunctions";
import Text from 'Common/Types/Text'; import Text from "Common/Types/Text";
import API from 'Common/Utils/API'; import API from "Common/Utils/API";
import LocalCache from 'CommonServer/Infrastructure/LocalCache'; import LocalCache from "CommonServer/Infrastructure/LocalCache";
import Markdown, { MarkdownContentType } from 'CommonServer/Types/Markdown'; import Markdown, { MarkdownContentType } from "CommonServer/Types/Markdown";
export interface BlogPostAuthor { export interface BlogPostAuthor {
username: string; username: string;
githubUrl: string; githubUrl: string;
profileImageUrl: string; profileImageUrl: string;
name: string; name: string;
} }
export interface BlogPostBaseProps { export interface BlogPostBaseProps {
title: string; title: string;
description: string; description: string;
formattedPostDate: string; formattedPostDate: string;
fileName: string; fileName: string;
tags: string[]; tags: string[];
postDate: string; postDate: string;
blogUrl: string; blogUrl: string;
} }
export interface BlogPostHeader extends BlogPostBaseProps { export interface BlogPostHeader extends BlogPostBaseProps {
authorGitHubUsername: string; authorGitHubUsername: string;
} }
export interface BlogPost extends BlogPostBaseProps { export interface BlogPost extends BlogPostBaseProps {
htmlBody: string; htmlBody: string;
markdownBody: string; markdownBody: string;
socialMediaImageUrl: string; socialMediaImageUrl: string;
author: BlogPostAuthor | null; author: BlogPostAuthor | null;
} }
const GitHubRawUrl: string = const GitHubRawUrl: string =
'https://raw.githubusercontent.com/oneuptime/blog/master'; "https://raw.githubusercontent.com/oneuptime/blog/master";
export default class BlogPostUtil { export default class BlogPostUtil {
public static async getBlogPostList( public static async getBlogPostList(
tagName?: string | undefined tagName?: string | undefined,
): Promise<BlogPostHeader[]> { ): Promise<BlogPostHeader[]> {
const fileUrl: URL = URL.fromString(`${GitHubRawUrl}/Blogs.json`); const fileUrl: URL = URL.fromString(`${GitHubRawUrl}/Blogs.json`);
const fileData: const fileData:
| HTTPResponse< | HTTPResponse<
| JSONObjectOrArray | JSONObjectOrArray
| BaseModel | BaseModel
| BaseModel[] | BaseModel[]
| AnalyticsBaseModel | AnalyticsBaseModel
| AnalyticsBaseModel[] | AnalyticsBaseModel[]
> >
| HTTPErrorResponse = await API.get(fileUrl); | HTTPErrorResponse = await API.get(fileUrl);
if (fileData.isFailure()) { if (fileData.isFailure()) {
throw fileData as HTTPErrorResponse; throw fileData as HTTPErrorResponse;
}
let jsonContent: string | JSONArray =
(fileData.data as string | JSONArray) || [];
if (typeof jsonContent === 'string') {
jsonContent = JSONFunctions.parseJSONArray(jsonContent);
}
const blogs: Array<JSONObject> = JSONFunctions.deserializeArray(
jsonContent as Array<JSONObject>
).reverse(); // reverse so new content comes first
const resultList: Array<BlogPostHeader> = [];
for (const blog of blogs) {
const fileName: string = blog['post'] as string;
const formattedPostDate: string =
this.getFormattedPostDateFromFileName(fileName);
const postDate: string = this.getPostDateFromFileName(fileName);
resultList.push({
title: blog['title'] as string,
description: blog['description'] as string,
fileName,
formattedPostDate,
postDate,
tags: blog['tags'] as string[],
authorGitHubUsername: blog['authorGitHubUsername'] as string,
blogUrl: `/blog/post/${fileName}`,
});
}
if (tagName) {
return resultList.filter((blog: BlogPostHeader) => {
return blog.tags
.map((item: string) => {
return Text.replaceAll(item.toLowerCase(), ' ', '-');
})
.includes(tagName);
});
}
return resultList;
} }
public static async getBlogPost( let jsonContent: string | JSONArray =
fileName: string (fileData.data as string | JSONArray) || [];
): Promise<BlogPost | null> {
let blogPost: BlogPost | null = this.getBlogPostFromCache(fileName);
// if (blogPost) { if (typeof jsonContent === "string") {
// return Promise.resolve(blogPost); jsonContent = JSONFunctions.parseJSONArray(jsonContent);
// }
blogPost = await this.getBlogPostFromGitHub(fileName);
// save this to cache
LocalCache.setJSON(
'blog',
fileName,
JSONFunctions.serialize(blogPost as any)
);
return blogPost;
} }
public static async getNameOfGitHubUser(username: string): Promise<string> { const blogs: Array<JSONObject> = JSONFunctions.deserializeArray(
const fileUrl: URL = URL.fromString( jsonContent as Array<JSONObject>,
`https://api.github.com/users/${username}` ).reverse(); // reverse so new content comes first
);
const fileData: const resultList: Array<BlogPostHeader> = [];
| HTTPResponse<
| JSONObjectOrArray
| BaseModel
| BaseModel[]
| AnalyticsBaseModel
| AnalyticsBaseModel[]
>
| HTTPErrorResponse = await API.get(fileUrl);
if (fileData.isFailure()) { for (const blog of blogs) {
throw fileData as HTTPErrorResponse; const fileName: string = blog["post"] as string;
} const formattedPostDate: string =
this.getFormattedPostDateFromFileName(fileName);
const postDate: string = this.getPostDateFromFileName(fileName);
const name: string = resultList.push({
(fileData.data as JSONObject)?.['name']?.toString() || ''; title: blog["title"] as string,
return name; description: blog["description"] as string,
fileName,
formattedPostDate,
postDate,
tags: blog["tags"] as string[],
authorGitHubUsername: blog["authorGitHubUsername"] as string,
blogUrl: `/blog/post/${fileName}`,
});
} }
public static async getGitHubMarkdownFileContent( if (tagName) {
githubPath: string return resultList.filter((blog: BlogPostHeader) => {
): Promise<string | null> { return blog.tags
const fileUrl: URL = URL.fromString(`${GitHubRawUrl}/${githubPath}`); .map((item: string) => {
return Text.replaceAll(item.toLowerCase(), " ", "-");
const fileData: })
| HTTPResponse< .includes(tagName);
| JSONObjectOrArray });
| BaseModel
| BaseModel[]
| AnalyticsBaseModel
| AnalyticsBaseModel[]
>
| HTTPErrorResponse = await API.get(fileUrl);
if (fileData.isFailure()) {
if ((fileData as HTTPErrorResponse).statusCode === 404) {
return null;
}
throw fileData as HTTPErrorResponse;
}
const markdownContent: string =
(fileData.data as JSONObject)?.['data']?.toString() || '';
return markdownContent;
} }
public static async getTags(): Promise<string[]> { return resultList;
// check if tags are in cache }
let tags: string[] = LocalCache.getJSON(
'blog-tags',
'tags'
) as string[];
if (tags && tags.length > 0) { public static async getBlogPost(fileName: string): Promise<BlogPost | null> {
return tags; let blogPost: BlogPost | null = this.getBlogPostFromCache(fileName);
}
tags = await this.getAllTagsFromGitHub(); // if (blogPost) {
// return Promise.resolve(blogPost);
// }
// save this to cache blogPost = await this.getBlogPostFromGitHub(fileName);
LocalCache.setJSON( // save this to cache
'blog-tags', LocalCache.setJSON(
'tags', "blog",
JSONFunctions.serialize(tags as any) fileName,
); JSONFunctions.serialize(blogPost as any),
);
return tags; return blogPost;
}
public static async getNameOfGitHubUser(username: string): Promise<string> {
const fileUrl: URL = URL.fromString(
`https://api.github.com/users/${username}`,
);
const fileData:
| HTTPResponse<
| JSONObjectOrArray
| BaseModel
| BaseModel[]
| AnalyticsBaseModel
| AnalyticsBaseModel[]
>
| HTTPErrorResponse = await API.get(fileUrl);
if (fileData.isFailure()) {
throw fileData as HTTPErrorResponse;
} }
public static async getAllTagsFromGitHub(): Promise<string[]> { const name: string =
const tagsMarkdownContent: string | null = (fileData.data as JSONObject)?.["name"]?.toString() || "";
await this.getGitHubMarkdownFileContent('Tags.md'); return name;
}
if (!tagsMarkdownContent) { public static async getGitHubMarkdownFileContent(
return []; githubPath: string,
} ): Promise<string | null> {
const fileUrl: URL = URL.fromString(`${GitHubRawUrl}/${githubPath}`);
const tags: Array<string> = tagsMarkdownContent const fileData:
.split('\n') | HTTPResponse<
.map((tag: string) => { | JSONObjectOrArray
return tag.trim(); | BaseModel
}) | BaseModel[]
.filter((tag: string) => { | AnalyticsBaseModel
return tag.startsWith('-'); | AnalyticsBaseModel[]
}) >
.map((tag: string) => { | HTTPErrorResponse = await API.get(fileUrl);
return tag.replace('-', '').trim();
});
return tags; if (fileData.isFailure()) {
if ((fileData as HTTPErrorResponse).statusCode === 404) {
return null;
}
throw fileData as HTTPErrorResponse;
} }
public static async getBlogPostFromGitHub( const markdownContent: string =
fileName: string (fileData.data as JSONObject)?.["data"]?.toString() || "";
): Promise<BlogPost | null> { return markdownContent;
const fileUrl: URL = URL.fromString( }
`${GitHubRawUrl}/posts/${fileName}/README.md`
);
const postDate: string = this.getPostDateFromFileName(fileName); public static async getTags(): Promise<string[]> {
const formattedPostDate: string = // check if tags are in cache
this.getFormattedPostDateFromFileName(fileName); let tags: string[] = LocalCache.getJSON("blog-tags", "tags") as string[];
const fileData: if (tags && tags.length > 0) {
| HTTPResponse< return tags;
| JSONObjectOrArray
| BaseModel
| BaseModel[]
| AnalyticsBaseModel
| AnalyticsBaseModel[]
>
| HTTPErrorResponse = await API.get(fileUrl);
if (fileData.isFailure()) {
if ((fileData as HTTPErrorResponse).statusCode === 404) {
return null;
}
throw fileData as HTTPErrorResponse;
}
let markdownContent: string =
(fileData.data as JSONObject)?.['data']?.toString() || '';
const blogPostAuthor: BlogPostAuthor | null =
await this.getAuthorFromFileContent(markdownContent);
const title: string = this.getTitleFromFileContent(markdownContent);
const description: string =
this.getDescriptionFromFileContent(markdownContent);
const tags: Array<string> =
this.getTagsFromFileContent(markdownContent);
markdownContent = this.getPostFromMarkdown(markdownContent);
const htmlBody: string = await Markdown.convertToHTML(
markdownContent,
MarkdownContentType.Blog
);
const blogPost: BlogPost = {
title,
description,
author: blogPostAuthor,
htmlBody,
markdownBody: markdownContent,
fileName,
tags,
postDate,
formattedPostDate,
socialMediaImageUrl: `${GitHubRawUrl}/posts/${fileName}/social-media.png`,
blogUrl: `https://oneuptime.com/blog/post/${fileName}`, // this has to be oneuptime.com because its used in twitter cards and faceboomk cards. Please dont change this.
};
return blogPost;
} }
private static getPostDateFromFileName(fileName: string): string { tags = await this.getAllTagsFromGitHub();
const year: string | undefined = fileName.split('-')[0];
const month: string | undefined = fileName.split('-')[1];
const day: string | undefined = fileName.split('-')[2];
if (!year || !month || !day) { // save this to cache
throw new BadDataException('Invalid file name');
}
return `${year}-${month}-${day}`; LocalCache.setJSON(
"blog-tags",
"tags",
JSONFunctions.serialize(tags as any),
);
return tags;
}
public static async getAllTagsFromGitHub(): Promise<string[]> {
const tagsMarkdownContent: string | null =
await this.getGitHubMarkdownFileContent("Tags.md");
if (!tagsMarkdownContent) {
return [];
} }
private static getFormattedPostDateFromFileName(fileName: string): string { const tags: Array<string> = tagsMarkdownContent
// file name is of the format YYYY-MM-DD-Title.md .split("\n")
const year: string | undefined = fileName.split('-')[0]; .map((tag: string) => {
const month: string | undefined = fileName.split('-')[1]; return tag.trim();
const day: string | undefined = fileName.split('-')[2]; })
.filter((tag: string) => {
return tag.startsWith("-");
})
.map((tag: string) => {
return tag.replace("-", "").trim();
});
if (!year || !month || !day) { return tags;
throw new BadDataException('Invalid file name'); }
}
const date: Date = OneUptimeDate.getDateFromYYYYMMDD(year, month, day); public static async getBlogPostFromGitHub(
return OneUptimeDate.getDateAsLocalFormattedString(date, true); fileName: string,
): Promise<BlogPost | null> {
const fileUrl: URL = URL.fromString(
`${GitHubRawUrl}/posts/${fileName}/README.md`,
);
const postDate: string = this.getPostDateFromFileName(fileName);
const formattedPostDate: string =
this.getFormattedPostDateFromFileName(fileName);
const fileData:
| HTTPResponse<
| JSONObjectOrArray
| BaseModel
| BaseModel[]
| AnalyticsBaseModel
| AnalyticsBaseModel[]
>
| HTTPErrorResponse = await API.get(fileUrl);
if (fileData.isFailure()) {
if ((fileData as HTTPErrorResponse).statusCode === 404) {
return null;
}
throw fileData as HTTPErrorResponse;
} }
private static getPostFromMarkdown(markdownContent: string): string { let markdownContent: string =
const authorLine: string | undefined = markdownContent (fileData.data as JSONObject)?.["data"]?.toString() || "";
.split('\n')
.find((line: string) => {
return line.startsWith('Author:');
});
const titleLine: string | undefined = markdownContent
.split('\n')
.find((line: string) => {
return line.startsWith('#');
});
const descriptionLine: string | undefined =
markdownContent.split('\n').find((line: string) => {
return line.startsWith('Description:');
}) || '';
const tagsLine: string | undefined = const blogPostAuthor: BlogPostAuthor | null =
markdownContent.split('\n').find((line: string) => { await this.getAuthorFromFileContent(markdownContent);
return line.startsWith('Tags:');
}) || '';
if (!authorLine && !titleLine && !descriptionLine && !tagsLine) { const title: string = this.getTitleFromFileContent(markdownContent);
return markdownContent; const description: string =
} this.getDescriptionFromFileContent(markdownContent);
const tags: Array<string> = this.getTagsFromFileContent(markdownContent);
const lines: string[] = markdownContent.split('\n'); markdownContent = this.getPostFromMarkdown(markdownContent);
if (authorLine) { const htmlBody: string = await Markdown.convertToHTML(
const authorLineIndex: number = lines.indexOf(authorLine); markdownContent,
lines.splice(authorLineIndex, 1); MarkdownContentType.Blog,
} );
if (titleLine) { const blogPost: BlogPost = {
const titleLineIndex: number = lines.indexOf(titleLine); title,
lines.splice(titleLineIndex, 1); description,
} author: blogPostAuthor,
htmlBody,
markdownBody: markdownContent,
fileName,
tags,
postDate,
formattedPostDate,
socialMediaImageUrl: `${GitHubRawUrl}/posts/${fileName}/social-media.png`,
blogUrl: `https://oneuptime.com/blog/post/${fileName}`, // this has to be oneuptime.com because its used in twitter cards and faceboomk cards. Please dont change this.
};
if (descriptionLine) { return blogPost;
const descriptionLineIndex: number = lines.indexOf(descriptionLine); }
lines.splice(descriptionLineIndex, 1);
}
if (tagsLine) { private static getPostDateFromFileName(fileName: string): string {
const tagsLineIndex: number = lines.indexOf(tagsLine); const year: string | undefined = fileName.split("-")[0];
lines.splice(tagsLineIndex, 1); const month: string | undefined = fileName.split("-")[1];
} const day: string | undefined = fileName.split("-")[2];
return lines.join('\n').trim(); if (!year || !month || !day) {
throw new BadDataException("Invalid file name");
} }
public static getBlogPostFromCache(fileName: string): BlogPost | null { return `${year}-${month}-${day}`;
const blogPost: BlogPost | null = LocalCache.getJSON( }
'blog',
fileName private static getFormattedPostDateFromFileName(fileName: string): string {
) as BlogPost | null; // file name is of the format YYYY-MM-DD-Title.md
return blogPost; const year: string | undefined = fileName.split("-")[0];
const month: string | undefined = fileName.split("-")[1];
const day: string | undefined = fileName.split("-")[2];
if (!year || !month || !day) {
throw new BadDataException("Invalid file name");
} }
public static getTitleFromFileContent(fileContent: string): string { const date: Date = OneUptimeDate.getDateFromYYYYMMDD(year, month, day);
// title is the first line that stars with "#" return OneUptimeDate.getDateAsLocalFormattedString(date, true);
}
const titleLine: string = private static getPostFromMarkdown(markdownContent: string): string {
fileContent const authorLine: string | undefined = markdownContent
.split('\n') .split("\n")
.find((line: string) => { .find((line: string) => {
return line.startsWith('#'); return line.startsWith("Author:");
}) });
?.replace('#', '') || 'OneUptime Blog'; const titleLine: string | undefined = markdownContent
.split("\n")
.find((line: string) => {
return line.startsWith("#");
});
const descriptionLine: string | undefined =
markdownContent.split("\n").find((line: string) => {
return line.startsWith("Description:");
}) || "";
return titleLine; const tagsLine: string | undefined =
markdownContent.split("\n").find((line: string) => {
return line.startsWith("Tags:");
}) || "";
if (!authorLine && !titleLine && !descriptionLine && !tagsLine) {
return markdownContent;
} }
public static getTagsFromFileContent(fileContent: string): string[] { const lines: string[] = markdownContent.split("\n");
// tags is the first line that starts with "Tags:"
const tagsLine: string | undefined = if (authorLine) {
fileContent const authorLineIndex: number = lines.indexOf(authorLine);
.split('\n') lines.splice(authorLineIndex, 1);
.find((line: string) => {
return line.startsWith('Tags:');
})
?.replace('Tags:', '') || '';
return tagsLine.split(',').map((tag: string) => {
return tag.trim();
});
} }
public static getDescriptionFromFileContent(fileContent: string): string { if (titleLine) {
// description is the first line that starts with ">" const titleLineIndex: number = lines.indexOf(titleLine);
lines.splice(titleLineIndex, 1);
const descriptionLine: string | undefined =
fileContent
.split('\n')
.find((line: string) => {
return line.startsWith('Description:');
})
?.replace('Description:', '') || '';
return descriptionLine;
} }
public static async getAuthorFromFileContent( if (descriptionLine) {
fileContent: string const descriptionLineIndex: number = lines.indexOf(descriptionLine);
): Promise<BlogPostAuthor | null> { lines.splice(descriptionLineIndex, 1);
// author line is in this format: Author: [username](githubUrl)
const authorLine: string | undefined = fileContent
.split('\n')
.find((line: string) => {
return line.startsWith('Author:');
});
const authorUsername: string | undefined = authorLine
?.split('[')[1]
?.split(']')[0];
const authorGitHubUrl: string | undefined = authorLine
?.split('(')[1]
?.split(')')[0];
const authorProfileImageUrl: string = `https://avatars.githubusercontent.com/${authorUsername}`;
if (!authorUsername || !authorGitHubUrl) {
return null;
}
return {
username: authorUsername,
githubUrl: authorGitHubUrl,
profileImageUrl: authorProfileImageUrl,
name: await this.getNameOfGitHubUser(authorUsername),
};
} }
if (tagsLine) {
const tagsLineIndex: number = lines.indexOf(tagsLine);
lines.splice(tagsLineIndex, 1);
}
return lines.join("\n").trim();
}
public static getBlogPostFromCache(fileName: string): BlogPost | null {
const blogPost: BlogPost | null = LocalCache.getJSON(
"blog",
fileName,
) as BlogPost | null;
return blogPost;
}
public static getTitleFromFileContent(fileContent: string): string {
// title is the first line that stars with "#"
const titleLine: string =
fileContent
.split("\n")
.find((line: string) => {
return line.startsWith("#");
})
?.replace("#", "") || "OneUptime Blog";
return titleLine;
}
public static getTagsFromFileContent(fileContent: string): string[] {
// tags is the first line that starts with "Tags:"
const tagsLine: string | undefined =
fileContent
.split("\n")
.find((line: string) => {
return line.startsWith("Tags:");
})
?.replace("Tags:", "") || "";
return tagsLine.split(",").map((tag: string) => {
return tag.trim();
});
}
public static getDescriptionFromFileContent(fileContent: string): string {
// description is the first line that starts with ">"
const descriptionLine: string | undefined =
fileContent
.split("\n")
.find((line: string) => {
return line.startsWith("Description:");
})
?.replace("Description:", "") || "";
return descriptionLine;
}
public static async getAuthorFromFileContent(
fileContent: string,
): Promise<BlogPostAuthor | null> {
// author line is in this format: Author: [username](githubUrl)
const authorLine: string | undefined = fileContent
.split("\n")
.find((line: string) => {
return line.startsWith("Author:");
});
const authorUsername: string | undefined = authorLine
?.split("[")[1]
?.split("]")[0];
const authorGitHubUrl: string | undefined = authorLine
?.split("(")[1]
?.split(")")[0];
const authorProfileImageUrl: string = `https://avatars.githubusercontent.com/${authorUsername}`;
if (!authorUsername || !authorGitHubUrl) {
return null;
}
return {
username: authorUsername,
githubUrl: authorGitHubUrl,
profileImageUrl: authorProfileImageUrl,
name: await this.getNameOfGitHubUser(authorUsername),
};
}
} }

View File

@@ -1,2 +1,2 @@
export const ViewsPath: string = '/usr/src/app/FeatureSet/Home/Views'; export const ViewsPath: string = "/usr/src/app/FeatureSet/Home/Views";
export const StaticPath: string = '/usr/src/app/FeatureSet/Home/Static'; export const StaticPath: string = "/usr/src/app/FeatureSet/Home/Static";

View File

@@ -1,15 +1,15 @@
import { ViewsPath } from './Config'; import { ViewsPath } from "./Config";
import { ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressResponse } from "CommonServer/Utils/Express";
export default class NotFoundUtil { export default class NotFoundUtil {
public static renderNotFound(res: ExpressResponse): void { public static renderNotFound(res: ExpressResponse): void {
res.status(404); res.status(404);
res.render(`${ViewsPath}/not-found.ejs`, { res.render(`${ViewsPath}/not-found.ejs`, {
footerCards: false, footerCards: false,
support: false, support: false,
cta: false, cta: false,
blackLogo: false, blackLogo: false,
requestDemoCta: false, requestDemoCta: false,
}); });
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,15 @@
import { ViewsPath } from './Config'; import { ViewsPath } from "./Config";
import { ExpressResponse } from 'CommonServer/Utils/Express'; import { ExpressResponse } from "CommonServer/Utils/Express";
export default class ServerErrorUtil { export default class ServerErrorUtil {
public static renderServerError(res: ExpressResponse): void { public static renderServerError(res: ExpressResponse): void {
res.status(500); res.status(500);
res.render(`${ViewsPath}/server-error.ejs`, { res.render(`${ViewsPath}/server-error.ejs`, {
footerCards: false, footerCards: false,
support: false, support: false,
cta: false, cta: false,
blackLogo: false, blackLogo: false,
requestDemoCta: false, requestDemoCta: false,
}); });
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,79 +1,79 @@
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import ResellerService from 'CommonServer/Services/ResellerService'; import ResellerService from "CommonServer/Services/ResellerService";
import Express, { import Express, {
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressRouter, ExpressRouter,
NextFunction, NextFunction,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import JSONWebToken from 'CommonServer/Utils/JsonWebToken'; import JSONWebToken from "CommonServer/Utils/JsonWebToken";
import Response from 'CommonServer/Utils/Response'; import Response from "CommonServer/Utils/Response";
import Reseller from 'Model/Models/Reseller'; import Reseller from "Model/Models/Reseller";
const router: ExpressRouter = Express.getRouter(); const router: ExpressRouter = Express.getRouter();
router.post( router.post(
'/reseller/auth/:resellerid', "/reseller/auth/:resellerid",
async ( async (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse, res: ExpressResponse,
next: NextFunction next: NextFunction,
): Promise<void> => { ): Promise<void> => {
try { try {
const resellerId: string | undefined = req.params['resellerid']; const resellerId: string | undefined = req.params["resellerid"];
if (!resellerId) { if (!resellerId) {
throw new BadDataException('Reseller ID not found'); throw new BadDataException("Reseller ID not found");
} }
const username: string = req.body['username']; const username: string = req.body["username"];
const password: string = req.body['password']; const password: string = req.body["password"];
if (!username) { if (!username) {
throw new BadDataException('Username not found'); throw new BadDataException("Username not found");
} }
if (!password) { if (!password) {
throw new BadDataException('Password not found'); throw new BadDataException("Password not found");
} }
// get the reseller user. // get the reseller user.
const reseller: Reseller | null = await ResellerService.findOneBy({ const reseller: Reseller | null = await ResellerService.findOneBy({
query: { query: {
resellerId: resellerId, resellerId: resellerId,
username: username, username: username,
password: password, password: password,
}, },
select: { select: {
_id: true, _id: true,
resellerId: true, resellerId: true,
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
if (!reseller) { if (!reseller) {
throw new BadDataException( throw new BadDataException(
'Reseller not found or username and password is incorrect' "Reseller not found or username and password is incorrect",
); );
} }
// if found then generate a token and return it. // if found then generate a token and return it.
const token: string = JSONWebToken.sign({ const token: string = JSONWebToken.sign({
data: { resellerId: resellerId }, data: { resellerId: resellerId },
expiresInSeconds: OneUptimeDate.getDayInSeconds(365), expiresInSeconds: OneUptimeDate.getDayInSeconds(365),
}); });
return Response.sendJsonObjectResponse(req, res, { return Response.sendJsonObjectResponse(req, res, {
access: token, access: token,
}); });
} catch (err) { } catch (err) {
return next(err); return next(err);
}
} }
},
); );
export default router; export default router;

View File

@@ -1,471 +1,436 @@
import AuthenticationEmail from '../Utils/AuthenticationEmail'; import AuthenticationEmail from "../Utils/AuthenticationEmail";
import SSOUtil from '../Utils/SSO'; import SSOUtil from "../Utils/SSO";
import { DashboardRoute } from 'Common/ServiceRoute'; import { DashboardRoute } from "Common/ServiceRoute";
import Hostname from 'Common/Types/API/Hostname'; import Hostname from "Common/Types/API/Hostname";
import Protocol from 'Common/Types/API/Protocol'; import Protocol from "Common/Types/API/Protocol";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import Email from 'Common/Types/Email'; import Email from "Common/Types/Email";
import BadRequestException from 'Common/Types/Exception/BadRequestException'; import BadRequestException from "Common/Types/Exception/BadRequestException";
import Exception from 'Common/Types/Exception/Exception'; import Exception from "Common/Types/Exception/Exception";
import ServerException from 'Common/Types/Exception/ServerException'; import ServerException from "Common/Types/Exception/ServerException";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from 'Common/Types/PositiveNumber'; import PositiveNumber from "Common/Types/PositiveNumber";
import DatabaseConfig from 'CommonServer/DatabaseConfig'; import DatabaseConfig from "CommonServer/DatabaseConfig";
import { Host, HttpProtocol } from 'CommonServer/EnvironmentConfig'; import { Host, HttpProtocol } from "CommonServer/EnvironmentConfig";
import AccessTokenService from 'CommonServer/Services/AccessTokenService'; import AccessTokenService from "CommonServer/Services/AccessTokenService";
import ProjectSSOService from 'CommonServer/Services/ProjectSsoService'; import ProjectSSOService from "CommonServer/Services/ProjectSsoService";
import TeamMemberService from 'CommonServer/Services/TeamMemberService'; import TeamMemberService from "CommonServer/Services/TeamMemberService";
import UserService from 'CommonServer/Services/UserService'; import UserService from "CommonServer/Services/UserService";
import CookieUtil from 'CommonServer/Utils/Cookie'; import CookieUtil from "CommonServer/Utils/Cookie";
import Express, { import Express, {
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressRouter, ExpressRouter,
NextFunction, NextFunction,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import JSONWebToken from 'CommonServer/Utils/JsonWebToken'; import JSONWebToken from "CommonServer/Utils/JsonWebToken";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import Response from 'CommonServer/Utils/Response'; import Response from "CommonServer/Utils/Response";
import ProjectSSO from 'Model/Models/ProjectSso'; import ProjectSSO from "Model/Models/ProjectSso";
import TeamMember from 'Model/Models/TeamMember'; import TeamMember from "Model/Models/TeamMember";
import User from 'Model/Models/User'; import User from "Model/Models/User";
import xml2js from 'xml2js'; import xml2js from "xml2js";
const router: ExpressRouter = Express.getRouter(); const router: ExpressRouter = Express.getRouter();
router.get( router.get(
'/sso/:projectId/:projectSsoId', "/sso/:projectId/:projectSsoId",
async ( async (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse, res: ExpressResponse,
next: NextFunction next: NextFunction,
): Promise<void> => { ): Promise<void> => {
try { try {
if (!req.params['projectId']) { if (!req.params["projectId"]) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Project ID not found') new BadRequestException("Project ID not found"),
); );
} }
if (!req.params['projectSsoId']) { if (!req.params["projectSsoId"]) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Project SSO ID not found') new BadRequestException("Project SSO ID not found"),
); );
} }
const projectSSO: ProjectSSO | null = const projectSSO: ProjectSSO | null = await ProjectSSOService.findOneBy({
await ProjectSSOService.findOneBy({ query: {
query: { projectId: new ObjectID(req.params["projectId"]),
projectId: new ObjectID(req.params['projectId']), _id: req.params["projectSsoId"],
_id: req.params['projectSsoId'], isEnabled: true,
isEnabled: true, },
}, select: {
select: { _id: true,
_id: true, signOnURL: true,
signOnURL: true, issuerURL: true,
issuerURL: true, projectId: true,
projectId: true, },
}, props: {
props: { isRoot: true,
isRoot: true, },
}, });
});
if (!projectSSO) { if (!projectSSO) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('SSO Config not found') new BadRequestException("SSO Config not found"),
); );
} }
// redirect to Identity Provider. // redirect to Identity Provider.
if (!projectSSO.signOnURL) { if (!projectSSO.signOnURL) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Sign On URL not found') new BadRequestException("Sign On URL not found"),
); );
} }
if (!projectSSO.issuerURL) { if (!projectSSO.issuerURL) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Issuer not found') new BadRequestException("Issuer not found"),
); );
} }
const samlRequestUrl: URL = SSOUtil.createSAMLRequestUrl({ const samlRequestUrl: URL = SSOUtil.createSAMLRequestUrl({
acsUrl: URL.fromString( acsUrl: URL.fromString(
`${HttpProtocol}${Host}/identity/idp-login/${projectSSO.projectId?.toString()}/${projectSSO.id?.toString()}` `${HttpProtocol}${Host}/identity/idp-login/${projectSSO.projectId?.toString()}/${projectSSO.id?.toString()}`,
), ),
signOnUrl: projectSSO.signOnURL!, signOnUrl: projectSSO.signOnURL!,
issuerUrl: URL.fromString( issuerUrl: URL.fromString(
`${HttpProtocol}${Host}/${projectSSO.projectId?.toString()}/${projectSSO.id?.toString()}` `${HttpProtocol}${Host}/${projectSSO.projectId?.toString()}/${projectSSO.id?.toString()}`,
), ),
}); });
return Response.redirect(req, res, samlRequestUrl); return Response.redirect(req, res, samlRequestUrl);
} catch (err) { } catch (err) {
return next(err); return next(err);
}
} }
},
); );
router.get( router.get(
'/idp-login/:projectId/:projectSsoId', "/idp-login/:projectId/:projectSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => { async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
return await loginUserWithSso(req, res); return await loginUserWithSso(req, res);
} },
); );
router.post( router.post(
'/idp-login/:projectId/:projectSsoId', "/idp-login/:projectId/:projectSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => { async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
return await loginUserWithSso(req, res); return await loginUserWithSso(req, res);
} },
); );
type LoginUserWithSsoFunction = ( type LoginUserWithSsoFunction = (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
) => Promise<void>; ) => Promise<void>;
const loginUserWithSso: LoginUserWithSsoFunction = async ( const loginUserWithSso: LoginUserWithSsoFunction = async (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse res: ExpressResponse,
): Promise<void> => { ): Promise<void> => {
try { try {
const samlResponseBase64: string = req.body.SAMLResponse; const samlResponseBase64: string = req.body.SAMLResponse;
if (!samlResponseBase64) { if (!samlResponseBase64) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('SAMLResponse not found') new BadRequestException("SAMLResponse not found"),
); );
}
const samlResponse: string = Buffer.from(
samlResponseBase64,
'base64'
).toString();
const response: JSONObject = await xml2js.parseStringPromise(
samlResponse
);
let issuerUrl: string = '';
let email: Email | null = null;
if (!req.params['projectId']) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException('Project ID not found')
);
}
if (!req.params['projectSsoId']) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException('Project SSO ID not found')
);
}
const projectSSO: ProjectSSO | null = await ProjectSSOService.findOneBy(
{
query: {
projectId: new ObjectID(req.params['projectId']),
_id: req.params['projectSsoId'],
isEnabled: true,
},
select: {
signOnURL: true,
issuerURL: true,
publicCertificate: true,
teams: {
_id: true,
},
},
props: {
isRoot: true,
},
}
);
if (!projectSSO) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException('SSO Config not found')
);
}
// redirect to Identity Provider.
if (!projectSSO.issuerURL) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException('Issuer URL not found')
);
}
// redirect to Identity Provider.
if (!projectSSO.signOnURL) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException('Sign on URL not found')
);
}
if (!projectSSO.publicCertificate) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException('Public Certificate not found')
);
}
try {
SSOUtil.isPayloadValid(response);
if (
!SSOUtil.isSignatureValid(
samlResponse,
projectSSO.publicCertificate
)
) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException(
'Signature is not valid or Public Certificate configured with this SSO provider is not valid'
)
);
}
issuerUrl = SSOUtil.getIssuer(response);
email = SSOUtil.getEmail(response);
} catch (err: unknown) {
if (err instanceof Exception) {
return Response.sendErrorResponse(req, res, err);
}
return Response.sendErrorResponse(req, res, new ServerException());
}
if (projectSSO.issuerURL.toString() !== issuerUrl) {
logger.error(
'Issuer URL does not match. It should be ' +
projectSSO.issuerURL.toString() +
' but it is ' +
issuerUrl.toString()
);
return Response.sendErrorResponse(
req,
res,
new BadRequestException('Issuer URL does not match')
);
}
// Check if he already belongs to the project, If he does - then log in.
let alreadySavedUser: User | null = await UserService.findOneBy({
query: { email: email },
select: {
_id: true,
name: true,
email: true,
isMasterAdmin: true,
isEmailVerified: true,
profilePictureId: true,
},
props: {
isRoot: true,
},
});
let isNewUser: boolean = false;
if (!alreadySavedUser) {
// this should never happen because user is logged in before he signs in with SSO UNLESS he initiates the login though the IDP.
/// Create a user.
alreadySavedUser = await UserService.createByEmail({
email,
isEmailVerified: true,
generateRandomPassword: true,
props: {
isRoot: true,
},
});
isNewUser = true;
}
// If he does not then add him to teams that he should belong and log in.
// This should never happen because email is verified before he logs in with SSO.
if (!alreadySavedUser.isEmailVerified && !isNewUser) {
await AuthenticationEmail.sendVerificationEmail(alreadySavedUser!);
return Response.render(
req,
res,
'/usr/src/app/FeatureSet/Identity/Views/Message.ejs',
{
title: 'Email not verified.',
message:
'Email is not verified. We have sent you an email with the verification link. Please do not forget to check spam.',
}
);
}
// check if the user already belongs to the project
const teamMemberCount: PositiveNumber = await TeamMemberService.countBy(
{
query: {
projectId: new ObjectID(req.params['projectId'] as string),
userId: alreadySavedUser!.id!,
},
props: {
isRoot: true,
},
}
);
if (teamMemberCount.toNumber() === 0) {
// user not in project, add him to default teams.
if (!projectSSO.teams || projectSSO.teams.length === 0) {
return Response.render(
req,
res,
'/usr/src/app/FeatureSet/Identity/Views/Message.ejs',
{
title: 'No teams added.',
message:
'No teams have been added to this SSO config. Please contact your admin and have default teams added.',
}
);
}
for (const team of projectSSO.teams) {
// add user to team
let teamMember: TeamMember = new TeamMember();
teamMember.projectId = new ObjectID(
req.params['projectId'] as string
);
teamMember.userId = alreadySavedUser.id!;
teamMember.hasAcceptedInvitation = true;
teamMember.invitationAcceptedAt =
OneUptimeDate.getCurrentDate();
teamMember.teamId = team.id!;
teamMember = await TeamMemberService.create({
data: teamMember,
props: {
isRoot: true,
ignoreHooks: true,
},
});
}
}
const projectId: ObjectID = new ObjectID(
req.params['projectId'] as string
);
const ssoToken: string = JSONWebToken.sign({
data: {
userId: alreadySavedUser.id!,
projectId: projectId,
name: alreadySavedUser.name!,
email: email,
isMasterAdmin: false,
isGeneralLogin: false,
},
expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30)
),
});
const oneUptimeToken: string = JSONWebToken.signUserLoginToken({
tokenData: {
userId: alreadySavedUser.id!,
email: alreadySavedUser.email!,
name: alreadySavedUser.name!,
isMasterAdmin: alreadySavedUser.isMasterAdmin!,
isGlobalLogin: false, // This is a general login without SSO. So, we will set this to false. This will give access to all the projects that dont require SSO.
},
expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30)
),
});
// Set a cookie with token.
CookieUtil.setCookie(
res,
CookieUtil.getUserTokenKey(),
oneUptimeToken,
{
maxAge: OneUptimeDate.getMillisecondsInDays(
new PositiveNumber(30)
),
httpOnly: true,
}
);
CookieUtil.setCookie(
res,
CookieUtil.getUserSSOKey(projectId),
ssoToken,
{
maxAge: OneUptimeDate.getMillisecondsInDays(
new PositiveNumber(30)
),
httpOnly: true,
}
);
// Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(
alreadySavedUser.id!
);
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
logger.info('User logged in with SSO' + email.toString());
return Response.redirect(
req,
res,
new URL(
httpProtocol,
host,
new Route(DashboardRoute.toString()).addRoute(
'/' + req.params['projectId']
)
)
);
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, new ServerException());
} }
const samlResponse: string = Buffer.from(
samlResponseBase64,
"base64",
).toString();
const response: JSONObject = await xml2js.parseStringPromise(samlResponse);
let issuerUrl: string = "";
let email: Email | null = null;
if (!req.params["projectId"]) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Project ID not found"),
);
}
if (!req.params["projectSsoId"]) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Project SSO ID not found"),
);
}
const projectSSO: ProjectSSO | null = await ProjectSSOService.findOneBy({
query: {
projectId: new ObjectID(req.params["projectId"]),
_id: req.params["projectSsoId"],
isEnabled: true,
},
select: {
signOnURL: true,
issuerURL: true,
publicCertificate: true,
teams: {
_id: true,
},
},
props: {
isRoot: true,
},
});
if (!projectSSO) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("SSO Config not found"),
);
}
// redirect to Identity Provider.
if (!projectSSO.issuerURL) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Issuer URL not found"),
);
}
// redirect to Identity Provider.
if (!projectSSO.signOnURL) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Sign on URL not found"),
);
}
if (!projectSSO.publicCertificate) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Public Certificate not found"),
);
}
try {
SSOUtil.isPayloadValid(response);
if (
!SSOUtil.isSignatureValid(samlResponse, projectSSO.publicCertificate)
) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException(
"Signature is not valid or Public Certificate configured with this SSO provider is not valid",
),
);
}
issuerUrl = SSOUtil.getIssuer(response);
email = SSOUtil.getEmail(response);
} catch (err: unknown) {
if (err instanceof Exception) {
return Response.sendErrorResponse(req, res, err);
}
return Response.sendErrorResponse(req, res, new ServerException());
}
if (projectSSO.issuerURL.toString() !== issuerUrl) {
logger.error(
"Issuer URL does not match. It should be " +
projectSSO.issuerURL.toString() +
" but it is " +
issuerUrl.toString(),
);
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Issuer URL does not match"),
);
}
// Check if he already belongs to the project, If he does - then log in.
let alreadySavedUser: User | null = await UserService.findOneBy({
query: { email: email },
select: {
_id: true,
name: true,
email: true,
isMasterAdmin: true,
isEmailVerified: true,
profilePictureId: true,
},
props: {
isRoot: true,
},
});
let isNewUser: boolean = false;
if (!alreadySavedUser) {
// this should never happen because user is logged in before he signs in with SSO UNLESS he initiates the login though the IDP.
/// Create a user.
alreadySavedUser = await UserService.createByEmail({
email,
isEmailVerified: true,
generateRandomPassword: true,
props: {
isRoot: true,
},
});
isNewUser = true;
}
// If he does not then add him to teams that he should belong and log in.
// This should never happen because email is verified before he logs in with SSO.
if (!alreadySavedUser.isEmailVerified && !isNewUser) {
await AuthenticationEmail.sendVerificationEmail(alreadySavedUser!);
return Response.render(
req,
res,
"/usr/src/app/FeatureSet/Identity/Views/Message.ejs",
{
title: "Email not verified.",
message:
"Email is not verified. We have sent you an email with the verification link. Please do not forget to check spam.",
},
);
}
// check if the user already belongs to the project
const teamMemberCount: PositiveNumber = await TeamMemberService.countBy({
query: {
projectId: new ObjectID(req.params["projectId"] as string),
userId: alreadySavedUser!.id!,
},
props: {
isRoot: true,
},
});
if (teamMemberCount.toNumber() === 0) {
// user not in project, add him to default teams.
if (!projectSSO.teams || projectSSO.teams.length === 0) {
return Response.render(
req,
res,
"/usr/src/app/FeatureSet/Identity/Views/Message.ejs",
{
title: "No teams added.",
message:
"No teams have been added to this SSO config. Please contact your admin and have default teams added.",
},
);
}
for (const team of projectSSO.teams) {
// add user to team
let teamMember: TeamMember = new TeamMember();
teamMember.projectId = new ObjectID(req.params["projectId"] as string);
teamMember.userId = alreadySavedUser.id!;
teamMember.hasAcceptedInvitation = true;
teamMember.invitationAcceptedAt = OneUptimeDate.getCurrentDate();
teamMember.teamId = team.id!;
teamMember = await TeamMemberService.create({
data: teamMember,
props: {
isRoot: true,
ignoreHooks: true,
},
});
}
}
const projectId: ObjectID = new ObjectID(req.params["projectId"] as string);
const ssoToken: string = JSONWebToken.sign({
data: {
userId: alreadySavedUser.id!,
projectId: projectId,
name: alreadySavedUser.name!,
email: email,
isMasterAdmin: false,
isGeneralLogin: false,
},
expiresInSeconds: OneUptimeDate.getSecondsInDays(new PositiveNumber(30)),
});
const oneUptimeToken: string = JSONWebToken.signUserLoginToken({
tokenData: {
userId: alreadySavedUser.id!,
email: alreadySavedUser.email!,
name: alreadySavedUser.name!,
isMasterAdmin: alreadySavedUser.isMasterAdmin!,
isGlobalLogin: false, // This is a general login without SSO. So, we will set this to false. This will give access to all the projects that dont require SSO.
},
expiresInSeconds: OneUptimeDate.getSecondsInDays(new PositiveNumber(30)),
});
// Set a cookie with token.
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), oneUptimeToken, {
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: true,
});
CookieUtil.setCookie(res, CookieUtil.getUserSSOKey(projectId), ssoToken, {
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: true,
});
// Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
logger.info("User logged in with SSO" + email.toString());
return Response.redirect(
req,
res,
new URL(
httpProtocol,
host,
new Route(DashboardRoute.toString()).addRoute(
"/" + req.params["projectId"],
),
),
);
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, new ServerException());
}
}; };
export default router; export default router;

View File

@@ -1,432 +1,422 @@
import BaseModel from 'Common/Models/BaseModel'; import BaseModel from "Common/Models/BaseModel";
import { FileRoute } from 'Common/ServiceRoute'; import { FileRoute } from "Common/ServiceRoute";
import Hostname from 'Common/Types/API/Hostname'; import Hostname from "Common/Types/API/Hostname";
import Protocol from 'Common/Types/API/Protocol'; import Protocol from "Common/Types/API/Protocol";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import JSONFunctions from 'Common/Types/JSONFunctions'; import JSONFunctions from "Common/Types/JSONFunctions";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from 'Common/Types/PositiveNumber'; import PositiveNumber from "Common/Types/PositiveNumber";
import DatabaseConfig from 'CommonServer/DatabaseConfig'; import DatabaseConfig from "CommonServer/DatabaseConfig";
import { EncryptionSecret } from 'CommonServer/EnvironmentConfig'; import { EncryptionSecret } from "CommonServer/EnvironmentConfig";
import MailService from 'CommonServer/Services/MailService'; import MailService from "CommonServer/Services/MailService";
import StatusPagePrivateUserService from 'CommonServer/Services/StatusPagePrivateUserService'; import StatusPagePrivateUserService from "CommonServer/Services/StatusPagePrivateUserService";
import StatusPageService from 'CommonServer/Services/StatusPageService'; import StatusPageService from "CommonServer/Services/StatusPageService";
import CookieUtil from 'CommonServer/Utils/Cookie'; import CookieUtil from "CommonServer/Utils/Cookie";
import Express, { import Express, {
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressRouter, ExpressRouter,
NextFunction, NextFunction,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import JSONWebToken from 'CommonServer/Utils/JsonWebToken'; import JSONWebToken from "CommonServer/Utils/JsonWebToken";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import Response from 'CommonServer/Utils/Response'; import Response from "CommonServer/Utils/Response";
import StatusPage from 'Model/Models/StatusPage'; import StatusPage from "Model/Models/StatusPage";
import StatusPagePrivateUser from 'Model/Models/StatusPagePrivateUser'; import StatusPagePrivateUser from "Model/Models/StatusPagePrivateUser";
const router: ExpressRouter = Express.getRouter(); const router: ExpressRouter = Express.getRouter();
router.post( router.post(
'/logout/:statuspageid', "/logout/:statuspageid",
async ( async (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse, res: ExpressResponse,
next: NextFunction next: NextFunction,
): Promise<void> => { ): Promise<void> => {
try { try {
if (!req.params['statuspageid']) { if (!req.params["statuspageid"]) {
throw new BadDataException('Status Page ID is required.'); throw new BadDataException("Status Page ID is required.");
} }
const statusPageId: ObjectID = new ObjectID( const statusPageId: ObjectID = new ObjectID(
req.params['statuspageid'].toString() req.params["statuspageid"].toString(),
); );
CookieUtil.removeCookie( CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId)); // remove the cookie.
res,
CookieUtil.getUserTokenKey(statusPageId)
); // remove the cookie.
return Response.sendEmptySuccessResponse(req, res); return Response.sendEmptySuccessResponse(req, res);
} catch (err) { } catch (err) {
return next(err); return next(err);
}
} }
},
); );
router.post( router.post(
'/forgot-password', "/forgot-password",
async ( async (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse, res: ExpressResponse,
next: NextFunction next: NextFunction,
): Promise<void> => { ): Promise<void> => {
try { try {
const data: JSONObject = req.body['data']; const data: JSONObject = req.body["data"];
if (!data['email']) { if (!data["email"]) {
throw new BadDataException('Email is required.'); throw new BadDataException("Email is required.");
} }
const user: StatusPagePrivateUser = BaseModel.fromJSON( const user: StatusPagePrivateUser = BaseModel.fromJSON(
data as JSONObject, data as JSONObject,
StatusPagePrivateUser StatusPagePrivateUser,
) as StatusPagePrivateUser; ) as StatusPagePrivateUser;
if (!user.statusPageId) { if (!user.statusPageId) {
throw new BadDataException('Status Page ID is required.'); throw new BadDataException("Status Page ID is required.");
} }
const statusPage: StatusPage | null = const statusPage: StatusPage | null = await StatusPageService.findOneById(
await StatusPageService.findOneById({ {
id: user.statusPageId!, id: user.statusPageId!,
props: { props: {
isRoot: true, isRoot: true,
ignoreHooks: true, ignoreHooks: true,
}, },
select: { select: {
_id: true, _id: true,
name: true, name: true,
pageTitle: true, pageTitle: true,
logoFileId: true, logoFileId: true,
requireSsoForLogin: true, requireSsoForLogin: true,
projectId: true, projectId: true,
}, },
}); },
);
if (!statusPage) { if (!statusPage) {
throw new BadDataException('Status Page not found'); throw new BadDataException("Status Page not found");
} }
if (statusPage.requireSsoForLogin) { if (statusPage.requireSsoForLogin) {
throw new BadDataException( throw new BadDataException(
'Status Page supports authentication by SSO. You cannot use email and password for authentication.' "Status Page supports authentication by SSO. You cannot use email and password for authentication.",
); );
} }
const statusPageName: string | undefined = const statusPageName: string | undefined =
statusPage.pageTitle || statusPage.name; statusPage.pageTitle || statusPage.name;
const statusPageURL: string = const statusPageURL: string = await StatusPageService.getStatusPageURL(
await StatusPageService.getStatusPageURL(statusPage.id!); statusPage.id!,
);
const alreadySavedUser: StatusPagePrivateUser | null = const alreadySavedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({ await StatusPagePrivateUserService.findOneBy({
query: { query: {
email: user.email!, email: user.email!,
statusPageId: user.statusPageId!, statusPageId: user.statusPageId!,
}, },
select: { select: {
_id: true, _id: true,
password: true, password: true,
email: true, email: true,
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
if (alreadySavedUser) { if (alreadySavedUser) {
const token: string = ObjectID.generate().toString(); const token: string = ObjectID.generate().toString();
await StatusPagePrivateUserService.updateOneBy({ await StatusPagePrivateUserService.updateOneBy({
query: { query: {
_id: alreadySavedUser._id!, _id: alreadySavedUser._id!,
}, },
data: { data: {
resetPasswordToken: token, resetPasswordToken: token,
resetPasswordExpires: OneUptimeDate.getOneDayAfter(), resetPasswordExpires: OneUptimeDate.getOneDayAfter(),
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
const host: Hostname = await DatabaseConfig.getHost(); const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
await DatabaseConfig.getHttpProtocol();
MailService.sendMail( MailService.sendMail(
{ {
toEmail: user.email!, toEmail: user.email!,
subject: 'Password Reset Request for ' + statusPageName, subject: "Password Reset Request for " + statusPageName,
templateType: templateType: EmailTemplateType.StatusPageForgotPassword,
EmailTemplateType.StatusPageForgotPassword, vars: {
vars: { statusPageName: statusPageName!,
statusPageName: statusPageName!, logoUrl: statusPage.logoFileId
logoUrl: statusPage.logoFileId ? new URL(httpProtocol, host)
? new URL(httpProtocol, host) .addRoute(FileRoute)
.addRoute(FileRoute) .addRoute("/image/" + statusPage.logoFileId)
.addRoute( .toString()
'/image/' + statusPage.logoFileId : "",
) homeURL: statusPageURL,
.toString() tokenVerifyUrl: URL.fromString(statusPageURL)
: '', .addRoute("/reset-password/" + token)
homeURL: statusPageURL, .toString(),
tokenVerifyUrl: URL.fromString(statusPageURL) },
.addRoute('/reset-password/' + token) },
.toString(), {
}, projectId: statusPage.projectId!,
}, },
{ ).catch((err: Error) => {
projectId: statusPage.projectId!, logger.error(err);
} });
).catch((err: Error) => {
logger.error(err);
});
return Response.sendEmptySuccessResponse(req, res); return Response.sendEmptySuccessResponse(req, res);
} }
throw new BadDataException( throw new BadDataException(
`No user is registered with ${user.email?.toString()}` `No user is registered with ${user.email?.toString()}`,
); );
} catch (err) { } catch (err) {
return next(err); return next(err);
}
} }
},
); );
router.post( router.post(
'/reset-password', "/reset-password",
async ( async (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse, res: ExpressResponse,
next: NextFunction next: NextFunction,
): Promise<void> => { ): Promise<void> => {
try { try {
const data: JSONObject = JSONFunctions.deserialize( const data: JSONObject = JSONFunctions.deserialize(req.body["data"]);
req.body['data']
);
if (!data['statusPageId']) { if (!data["statusPageId"]) {
throw new BadDataException('Status Page ID is required.'); throw new BadDataException("Status Page ID is required.");
} }
const user: StatusPagePrivateUser = BaseModel.fromJSON( const user: StatusPagePrivateUser = BaseModel.fromJSON(
data as JSONObject, data as JSONObject,
StatusPagePrivateUser StatusPagePrivateUser,
) as StatusPagePrivateUser; ) as StatusPagePrivateUser;
await user.password?.hashValue(EncryptionSecret); await user.password?.hashValue(EncryptionSecret);
const alreadySavedUser: StatusPagePrivateUser | null = const alreadySavedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({ await StatusPagePrivateUserService.findOneBy({
query: { query: {
statusPageId: new ObjectID( statusPageId: new ObjectID(data["statusPageId"].toString()),
data['statusPageId'].toString() resetPasswordToken: (user.resetPasswordToken as string) || "",
), },
resetPasswordToken: select: {
(user.resetPasswordToken as string) || '', _id: true,
}, password: true,
select: { email: true,
_id: true, resetPasswordExpires: true,
password: true, },
email: true, props: {
resetPasswordExpires: true, isRoot: true,
}, },
props: { });
isRoot: true,
},
});
if (!alreadySavedUser) { if (!alreadySavedUser) {
throw new BadDataException( throw new BadDataException(
'Invalid link. Please go to forgot password page again and request a new link.' "Invalid link. Please go to forgot password page again and request a new link.",
); );
} }
if ( if (
alreadySavedUser && alreadySavedUser &&
OneUptimeDate.hasExpired(alreadySavedUser.resetPasswordExpires!) OneUptimeDate.hasExpired(alreadySavedUser.resetPasswordExpires!)
) { ) {
throw new BadDataException( throw new BadDataException(
'Expired link. Please go to forgot password page again and request a new link.' "Expired link. Please go to forgot password page again and request a new link.",
); );
} }
const statusPage: StatusPage | null = const statusPage: StatusPage | null = await StatusPageService.findOneById(
await StatusPageService.findOneById({ {
id: new ObjectID(data['statusPageId'].toString()), id: new ObjectID(data["statusPageId"].toString()),
props: { props: {
isRoot: true, isRoot: true,
ignoreHooks: true, ignoreHooks: true,
}, },
select: { select: {
_id: true, _id: true,
name: true, name: true,
pageTitle: true, pageTitle: true,
logoFileId: true, logoFileId: true,
requireSsoForLogin: true, requireSsoForLogin: true,
projectId: true, projectId: true,
}, },
}); },
);
if (!statusPage) { if (!statusPage) {
throw new BadDataException('Status Page not found'); throw new BadDataException("Status Page not found");
} }
if (statusPage.requireSsoForLogin) { if (statusPage.requireSsoForLogin) {
throw new BadDataException( throw new BadDataException(
'Status Page supports authentication by SSO. You cannot use email and password for authentication.' "Status Page supports authentication by SSO. You cannot use email and password for authentication.",
); );
} }
const statusPageName: string | undefined = const statusPageName: string | undefined =
statusPage.pageTitle || statusPage.name; statusPage.pageTitle || statusPage.name;
const statusPageURL: string = const statusPageURL: string = await StatusPageService.getStatusPageURL(
await StatusPageService.getStatusPageURL(statusPage.id!); statusPage.id!,
);
await StatusPagePrivateUserService.updateOneById({ await StatusPagePrivateUserService.updateOneById({
id: alreadySavedUser.id!, id: alreadySavedUser.id!,
data: { data: {
password: user.password!, password: user.password!,
resetPasswordToken: null!, resetPasswordToken: null!,
resetPasswordExpires: null!, resetPasswordExpires: null!,
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
const host: Hostname = await DatabaseConfig.getHost(); const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
await DatabaseConfig.getHttpProtocol();
MailService.sendMail( MailService.sendMail(
{ {
toEmail: alreadySavedUser.email!, toEmail: alreadySavedUser.email!,
subject: 'Password Changed.', subject: "Password Changed.",
templateType: EmailTemplateType.StatusPagePasswordChanged, templateType: EmailTemplateType.StatusPagePasswordChanged,
vars: { vars: {
homeURL: statusPageURL, homeURL: statusPageURL,
statusPageName: statusPageName || '', statusPageName: statusPageName || "",
logoUrl: statusPage.logoFileId logoUrl: statusPage.logoFileId
? new URL(httpProtocol, host) ? new URL(httpProtocol, host)
.addRoute(FileRoute) .addRoute(FileRoute)
.addRoute('/image/' + statusPage.logoFileId) .addRoute("/image/" + statusPage.logoFileId)
.toString() .toString()
: '', : "",
}, },
}, },
{ {
projectId: statusPage.projectId!, projectId: statusPage.projectId!,
} },
).catch((err: Error) => { ).catch((err: Error) => {
logger.error(err); logger.error(err);
}); });
return Response.sendEmptySuccessResponse(req, res); return Response.sendEmptySuccessResponse(req, res);
} catch (err) { } catch (err) {
return next(err); return next(err);
}
} }
},
); );
router.post( router.post(
'/login', "/login",
async ( async (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse, res: ExpressResponse,
next: NextFunction next: NextFunction,
): Promise<void> => { ): Promise<void> => {
try { try {
const data: JSONObject = req.body['data']; const data: JSONObject = req.body["data"];
const user: StatusPagePrivateUser = BaseModel.fromJSON( const user: StatusPagePrivateUser = BaseModel.fromJSON(
data as JSONObject, data as JSONObject,
StatusPagePrivateUser StatusPagePrivateUser,
) as StatusPagePrivateUser; ) as StatusPagePrivateUser;
if (!user.statusPageId) { if (!user.statusPageId) {
throw new BadDataException('Status Page ID not found'); throw new BadDataException("Status Page ID not found");
} }
const statusPage: StatusPage | null = const statusPage: StatusPage | null = await StatusPageService.findOneById(
await StatusPageService.findOneById({ {
id: user.statusPageId, id: user.statusPageId,
props: { props: {
isRoot: true, isRoot: true,
ignoreHooks: true, ignoreHooks: true,
}, },
select: { select: {
requireSsoForLogin: true, requireSsoForLogin: true,
}, },
}); },
);
if (!statusPage) { if (!statusPage) {
throw new BadDataException('Status Page not found'); throw new BadDataException("Status Page not found");
} }
if (statusPage.requireSsoForLogin) { if (statusPage.requireSsoForLogin) {
throw new BadDataException( throw new BadDataException(
'Status Page supports authentication by SSO. You cannot use email and password for authentication.' "Status Page supports authentication by SSO. You cannot use email and password for authentication.",
); );
} }
await user.password?.hashValue(EncryptionSecret); await user.password?.hashValue(EncryptionSecret);
const alreadySavedUser: StatusPagePrivateUser | null = const alreadySavedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({ await StatusPagePrivateUserService.findOneBy({
query: { query: {
email: user.email!, email: user.email!,
password: user.password!, password: user.password!,
statusPageId: user.statusPageId!, statusPageId: user.statusPageId!,
}, },
select: { select: {
_id: true, _id: true,
password: true, password: true,
email: true, email: true,
statusPageId: true, statusPageId: true,
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
if (alreadySavedUser) { if (alreadySavedUser) {
const token: string = JSONWebToken.sign({ const token: string = JSONWebToken.sign({
data: alreadySavedUser, data: alreadySavedUser,
expiresInSeconds: OneUptimeDate.getSecondsInDays( expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30) new PositiveNumber(30),
), ),
}); });
CookieUtil.setCookie( CookieUtil.setCookie(
res, res,
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!), CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
token, token,
{ {
httpOnly: true, httpOnly: true,
maxAge: OneUptimeDate.getMillisecondsInDays( maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
new PositiveNumber(30) },
), );
}
);
return Response.sendEntityResponse( return Response.sendEntityResponse(
req, req,
res, res,
alreadySavedUser, alreadySavedUser,
StatusPagePrivateUser, StatusPagePrivateUser,
{ {
miscData: { miscData: {
token: token, token: token,
}, },
} },
); );
} }
throw new BadDataException( throw new BadDataException(
'Invalid login: Email or password does not match.' "Invalid login: Email or password does not match.",
); );
} catch (err) { } catch (err) {
return next(err); return next(err);
}
} }
},
); );
export default router; export default router;

View File

@@ -1,323 +1,312 @@
import SSOUtil from '../Utils/SSO'; import SSOUtil from "../Utils/SSO";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import Email from 'Common/Types/Email'; import Email from "Common/Types/Email";
import BadRequestException from 'Common/Types/Exception/BadRequestException'; import BadRequestException from "Common/Types/Exception/BadRequestException";
import Exception from 'Common/Types/Exception/Exception'; import Exception from "Common/Types/Exception/Exception";
import ServerException from 'Common/Types/Exception/ServerException'; import ServerException from "Common/Types/Exception/ServerException";
import HashedString from 'Common/Types/HashedString'; import HashedString from "Common/Types/HashedString";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from 'Common/Types/PositiveNumber'; import PositiveNumber from "Common/Types/PositiveNumber";
import { Host, HttpProtocol } from 'CommonServer/EnvironmentConfig'; import { Host, HttpProtocol } from "CommonServer/EnvironmentConfig";
import StatusPagePrivateUserService from 'CommonServer/Services/StatusPagePrivateUserService'; import StatusPagePrivateUserService from "CommonServer/Services/StatusPagePrivateUserService";
import StatusPageService from 'CommonServer/Services/StatusPageService'; import StatusPageService from "CommonServer/Services/StatusPageService";
import StatusPageSsoService from 'CommonServer/Services/StatusPageSsoService'; import StatusPageSsoService from "CommonServer/Services/StatusPageSsoService";
import CookieUtil from 'CommonServer/Utils/Cookie'; import CookieUtil from "CommonServer/Utils/Cookie";
import Express, { import Express, {
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressRouter, ExpressRouter,
NextFunction, NextFunction,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import JSONWebToken from 'CommonServer/Utils/JsonWebToken'; import JSONWebToken from "CommonServer/Utils/JsonWebToken";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import Response from 'CommonServer/Utils/Response'; import Response from "CommonServer/Utils/Response";
import StatusPagePrivateUser from 'Model/Models/StatusPagePrivateUser'; import StatusPagePrivateUser from "Model/Models/StatusPagePrivateUser";
import StatusPageSSO from 'Model/Models/StatusPageSso'; import StatusPageSSO from "Model/Models/StatusPageSso";
import xml2js from 'xml2js'; import xml2js from "xml2js";
const router: ExpressRouter = Express.getRouter(); const router: ExpressRouter = Express.getRouter();
router.get( router.get(
'/status-page-sso/:statusPageId/:statusPageSsoId', "/status-page-sso/:statusPageId/:statusPageSsoId",
async ( async (
req: ExpressRequest, req: ExpressRequest,
res: ExpressResponse, res: ExpressResponse,
next: NextFunction next: NextFunction,
): Promise<void> => { ): Promise<void> => {
try { try {
if (!req.params['statusPageId']) { if (!req.params["statusPageId"]) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Status Page ID not found') new BadRequestException("Status Page ID not found"),
); );
} }
if (!req.params['statusPageSsoId']) { if (!req.params["statusPageSsoId"]) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Status Page SSO ID not found') new BadRequestException("Status Page SSO ID not found"),
); );
} }
const statusPageId: ObjectID = new ObjectID( const statusPageId: ObjectID = new ObjectID(req.params["statusPageId"]);
req.params['statusPageId']
);
const statusPageSSO: StatusPageSSO | null = const statusPageSSO: StatusPageSSO | null =
await StatusPageSsoService.findOneBy({ await StatusPageSsoService.findOneBy({
query: { query: {
statusPageId: statusPageId, statusPageId: statusPageId,
_id: req.params['statusPageSsoId'], _id: req.params["statusPageSsoId"],
isEnabled: true, isEnabled: true,
}, },
select: { select: {
signOnURL: true, signOnURL: true,
statusPageId: true, statusPageId: true,
_id: true, _id: true,
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
if (!statusPageSSO) { if (!statusPageSSO) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('SSO Config not found') new BadRequestException("SSO Config not found"),
); );
} }
// redirect to Identity Provider. // redirect to Identity Provider.
if (!statusPageSSO.signOnURL) { if (!statusPageSSO.signOnURL) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Sign On URL not found') new BadRequestException("Sign On URL not found"),
); );
} }
const samlRequestUrl: URL = SSOUtil.createSAMLRequestUrl({ const samlRequestUrl: URL = SSOUtil.createSAMLRequestUrl({
acsUrl: URL.fromString( acsUrl: URL.fromString(
`${HttpProtocol}${Host}/identity/status-page-idp-login/${statusPageSSO.statusPageId?.toString()}/${statusPageSSO.id?.toString()}` `${HttpProtocol}${Host}/identity/status-page-idp-login/${statusPageSSO.statusPageId?.toString()}/${statusPageSSO.id?.toString()}`,
), ),
signOnUrl: statusPageSSO.signOnURL!, signOnUrl: statusPageSSO.signOnURL!,
issuerUrl: URL.fromString( issuerUrl: URL.fromString(
`${HttpProtocol}${Host}/${statusPageSSO.statusPageId?.toString()}/${statusPageSSO.id?.toString()}` `${HttpProtocol}${Host}/${statusPageSSO.statusPageId?.toString()}/${statusPageSSO.id?.toString()}`,
), ),
}); });
return Response.redirect(req, res, samlRequestUrl); return Response.redirect(req, res, samlRequestUrl);
} catch (err) { } catch (err) {
return next(err); return next(err);
}
} }
},
); );
router.post( router.post(
'/status-page-idp-login/:statusPageId/:statusPageSsoId', "/status-page-idp-login/:statusPageId/:statusPageSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => { async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
try { try {
const samlResponseBase64: string = req.body.SAMLResponse; const samlResponseBase64: string = req.body.SAMLResponse;
const samlResponse: string = Buffer.from( const samlResponse: string = Buffer.from(
samlResponseBase64, samlResponseBase64,
'base64' "base64",
).toString(); ).toString();
const response: JSONObject = await xml2js.parseStringPromise( const response: JSONObject =
samlResponse await xml2js.parseStringPromise(samlResponse);
);
let issuerUrl: string = ''; let issuerUrl: string = "";
let email: Email | null = null; let email: Email | null = null;
if (!req.params['statusPageId']) { if (!req.params["statusPageId"]) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Status Page ID not found') new BadRequestException("Status Page ID not found"),
); );
} }
if (!req.params['statusPageSsoId']) { if (!req.params["statusPageSsoId"]) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Status Page SSO ID not found') new BadRequestException("Status Page SSO ID not found"),
); );
} }
const statusPageId: ObjectID = new ObjectID( const statusPageId: ObjectID = new ObjectID(req.params["statusPageId"]);
req.params['statusPageId']
);
const statusPageSSO: StatusPageSSO | null = const statusPageSSO: StatusPageSSO | null =
await StatusPageSsoService.findOneBy({ await StatusPageSsoService.findOneBy({
query: { query: {
statusPageId: statusPageId, statusPageId: statusPageId,
_id: req.params['statusPageSsoId'], _id: req.params["statusPageSsoId"],
isEnabled: true, isEnabled: true,
}, },
select: { select: {
signOnURL: true, signOnURL: true,
issuerURL: true, issuerURL: true,
publicCertificate: true, publicCertificate: true,
projectId: true, projectId: true,
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
if (!statusPageSSO) { if (!statusPageSSO) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('SSO Config not found') new BadRequestException("SSO Config not found"),
); );
} }
if (!statusPageSSO.projectId) { if (!statusPageSSO.projectId) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('SSO Config Project ID not found') new BadRequestException("SSO Config Project ID not found"),
); );
} }
const projectId: ObjectID = statusPageSSO.projectId; const projectId: ObjectID = statusPageSSO.projectId;
// redirect to Identity Provider. // redirect to Identity Provider.
if (!statusPageSSO.issuerURL) { if (!statusPageSSO.issuerURL) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Issuer URL not found') new BadRequestException("Issuer URL not found"),
); );
} }
// redirect to Identity Provider. // redirect to Identity Provider.
if (!statusPageSSO.signOnURL) { if (!statusPageSSO.signOnURL) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Sign on URL not found') new BadRequestException("Sign on URL not found"),
); );
} }
if (!statusPageSSO.publicCertificate) { if (!statusPageSSO.publicCertificate) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Public Certificate not found') new BadRequestException("Public Certificate not found"),
); );
} }
try { try {
SSOUtil.isPayloadValid(response); SSOUtil.isPayloadValid(response);
if ( if (
!SSOUtil.isSignatureValid( !SSOUtil.isSignatureValid(
samlResponse, samlResponse,
statusPageSSO.publicCertificate statusPageSSO.publicCertificate,
) )
) { ) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadRequestException('Signature is not valid') new BadRequestException("Signature is not valid"),
); );
}
issuerUrl = SSOUtil.getIssuer(response);
email = SSOUtil.getEmail(response);
} catch (err: unknown) {
if (err instanceof Exception) {
return Response.sendErrorResponse(req, res, err);
}
return Response.sendErrorResponse(
req,
res,
new ServerException()
);
}
if (statusPageSSO.issuerURL.toString() !== issuerUrl) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException('Issuer URL does not match')
);
}
// Check if he already belongs to the project, If he does - then log in.
let alreadySavedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({
query: { email: email, statusPageId: statusPageId },
select: {
_id: true,
email: true,
statusPageId: true,
projectId: true,
},
props: {
isRoot: true,
},
});
if (!alreadySavedUser) {
/// Create a user.
alreadySavedUser = new StatusPagePrivateUser();
alreadySavedUser.projectId = projectId;
alreadySavedUser.statusPageId = statusPageId;
alreadySavedUser.email = email;
alreadySavedUser.password = new HashedString(
ObjectID.generate().toString()
);
alreadySavedUser.isSsoUser = true;
alreadySavedUser = await StatusPagePrivateUserService.create({
data: alreadySavedUser,
props: { isRoot: true },
});
}
const token: string = JSONWebToken.sign({
data: alreadySavedUser,
expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30)
),
});
CookieUtil.setCookie(
res,
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
token,
{
httpOnly: true,
maxAge: OneUptimeDate.getMillisecondsInDays(
new PositiveNumber(30)
),
}
);
// get status page URL.
const statusPageURL: string =
await StatusPageService.getStatusPageFirstURL(statusPageId);
return Response.redirect(
req,
res,
URL.fromString(statusPageURL).addQueryParams({
token: token,
})
);
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, new ServerException());
} }
issuerUrl = SSOUtil.getIssuer(response);
email = SSOUtil.getEmail(response);
} catch (err: unknown) {
if (err instanceof Exception) {
return Response.sendErrorResponse(req, res, err);
}
return Response.sendErrorResponse(req, res, new ServerException());
}
if (statusPageSSO.issuerURL.toString() !== issuerUrl) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Issuer URL does not match"),
);
}
// Check if he already belongs to the project, If he does - then log in.
let alreadySavedUser: StatusPagePrivateUser | null =
await StatusPagePrivateUserService.findOneBy({
query: { email: email, statusPageId: statusPageId },
select: {
_id: true,
email: true,
statusPageId: true,
projectId: true,
},
props: {
isRoot: true,
},
});
if (!alreadySavedUser) {
/// Create a user.
alreadySavedUser = new StatusPagePrivateUser();
alreadySavedUser.projectId = projectId;
alreadySavedUser.statusPageId = statusPageId;
alreadySavedUser.email = email;
alreadySavedUser.password = new HashedString(
ObjectID.generate().toString(),
);
alreadySavedUser.isSsoUser = true;
alreadySavedUser = await StatusPagePrivateUserService.create({
data: alreadySavedUser,
props: { isRoot: true },
});
}
const token: string = JSONWebToken.sign({
data: alreadySavedUser,
expiresInSeconds: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30),
),
});
CookieUtil.setCookie(
res,
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
token,
{
httpOnly: true,
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
},
);
// get status page URL.
const statusPageURL: string =
await StatusPageService.getStatusPageFirstURL(statusPageId);
return Response.redirect(
req,
res,
URL.fromString(statusPageURL).addQueryParams({
token: token,
}),
);
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, new ServerException());
} }
},
); );
export default router; export default router;

View File

@@ -1,31 +1,31 @@
import AuthenticationAPI from './API/Authentication'; import AuthenticationAPI from "./API/Authentication";
import ResellerAPI from './API/Reseller'; import ResellerAPI from "./API/Reseller";
import SsoAPI from './API/SSO'; import SsoAPI from "./API/SSO";
import StatusPageAuthenticationAPI from './API/StatusPageAuthentication'; import StatusPageAuthenticationAPI from "./API/StatusPageAuthentication";
import StatusPageSsoAPI from './API/StatusPageSSO'; import StatusPageSsoAPI from "./API/StatusPageSSO";
import FeatureSet from 'CommonServer/Types/FeatureSet'; import FeatureSet from "CommonServer/Types/FeatureSet";
import Express, { ExpressApplication } from 'CommonServer/Utils/Express'; import Express, { ExpressApplication } from "CommonServer/Utils/Express";
import 'ejs'; import "ejs";
const IdentityFeatureSet: FeatureSet = { const IdentityFeatureSet: FeatureSet = {
init: async (): Promise<void> => { init: async (): Promise<void> => {
const app: ExpressApplication = Express.getExpressApp(); const app: ExpressApplication = Express.getExpressApp();
const APP_NAME: string = 'api/identity'; const APP_NAME: string = "api/identity";
app.use([`/${APP_NAME}`, '/'], AuthenticationAPI); app.use([`/${APP_NAME}`, "/"], AuthenticationAPI);
app.use([`/${APP_NAME}`, '/'], ResellerAPI); app.use([`/${APP_NAME}`, "/"], ResellerAPI);
app.use([`/${APP_NAME}`, '/'], SsoAPI); app.use([`/${APP_NAME}`, "/"], SsoAPI);
app.use([`/${APP_NAME}`, '/'], StatusPageSsoAPI); app.use([`/${APP_NAME}`, "/"], StatusPageSsoAPI);
app.use( app.use(
[`/${APP_NAME}/status-page`, '/status-page'], [`/${APP_NAME}/status-page`, "/status-page"],
StatusPageAuthenticationAPI StatusPageAuthenticationAPI,
); );
}, },
}; };
export default IdentityFeatureSet; export default IdentityFeatureSet;

View File

@@ -1,57 +1,57 @@
import { AccountsRoute } from 'Common/ServiceRoute'; import { AccountsRoute } from "Common/ServiceRoute";
import Hostname from 'Common/Types/API/Hostname'; import Hostname from "Common/Types/API/Hostname";
import Protocol from 'Common/Types/API/Protocol'; import Protocol from "Common/Types/API/Protocol";
import Route from 'Common/Types/API/Route'; import Route from "Common/Types/API/Route";
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import Email from 'Common/Types/Email'; import Email from "Common/Types/Email";
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import DatabaseConfig from 'CommonServer/DatabaseConfig'; import DatabaseConfig from "CommonServer/DatabaseConfig";
import EmailVerificationTokenService from 'CommonServer/Services/EmailVerificationTokenService'; import EmailVerificationTokenService from "CommonServer/Services/EmailVerificationTokenService";
import MailService from 'CommonServer/Services/MailService'; import MailService from "CommonServer/Services/MailService";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import EmailVerificationToken from 'Model/Models/EmailVerificationToken'; import EmailVerificationToken from "Model/Models/EmailVerificationToken";
import User from 'Model/Models/User'; import User from "Model/Models/User";
export default class AuthenticationEmail { export default class AuthenticationEmail {
public static async sendVerificationEmail(user: User): Promise<void> { public static async sendVerificationEmail(user: User): Promise<void> {
const generatedToken: ObjectID = ObjectID.generate(); const generatedToken: ObjectID = ObjectID.generate();
const emailVerificationToken: EmailVerificationToken = const emailVerificationToken: EmailVerificationToken =
new EmailVerificationToken(); new EmailVerificationToken();
emailVerificationToken.userId = user?.id as ObjectID; emailVerificationToken.userId = user?.id as ObjectID;
emailVerificationToken.email = user?.email as Email; emailVerificationToken.email = user?.email as Email;
emailVerificationToken.token = generatedToken; emailVerificationToken.token = generatedToken;
emailVerificationToken.expires = OneUptimeDate.getOneDayAfter(); emailVerificationToken.expires = OneUptimeDate.getOneDayAfter();
await EmailVerificationTokenService.create({ await EmailVerificationTokenService.create({
data: emailVerificationToken, data: emailVerificationToken,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
const host: Hostname = await DatabaseConfig.getHost(); const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol(); const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
MailService.sendMail({ MailService.sendMail({
toEmail: user.email!, toEmail: user.email!,
subject: 'Please verify email.', subject: "Please verify email.",
templateType: EmailTemplateType.SignupWelcomeEmail, templateType: EmailTemplateType.SignupWelcomeEmail,
vars: { vars: {
name: user.name?.toString() || '', name: user.name?.toString() || "",
tokenVerifyUrl: new URL( tokenVerifyUrl: new URL(
httpProtocol, httpProtocol,
host, host,
new Route(AccountsRoute.toString()).addRoute( new Route(AccountsRoute.toString()).addRoute(
'/verify-email/' + generatedToken.toString() "/verify-email/" + generatedToken.toString(),
) ),
).toString(), ).toString(),
homeUrl: new URL(httpProtocol, host).toString(), homeUrl: new URL(httpProtocol, host).toString(),
}, },
}).catch((err: Error) => { }).catch((err: Error) => {
logger.error(err); logger.error(err);
}); });
} }
} }

View File

@@ -1,230 +1,220 @@
import URL from 'Common/Types/API/URL'; import URL from "Common/Types/API/URL";
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import Email from 'Common/Types/Email'; import Email from "Common/Types/Email";
import BadRequestException from 'Common/Types/Exception/BadRequestException'; import BadRequestException from "Common/Types/Exception/BadRequestException";
import { JSONArray, JSONObject } from 'Common/Types/JSON'; import { JSONArray, JSONObject } from "Common/Types/JSON";
import Text from 'Common/Types/Text'; import Text from "Common/Types/Text";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import xmlCrypto, { FileKeyInfo } from 'xml-crypto'; import xmlCrypto, { FileKeyInfo } from "xml-crypto";
import xmldom from 'xmldom'; import xmldom from "xmldom";
import zlib from 'zlib'; import zlib from "zlib";
export default class SSOUtil { export default class SSOUtil {
public static createSAMLRequestUrl(data: { public static createSAMLRequestUrl(data: {
acsUrl: URL; acsUrl: URL;
signOnUrl: URL; signOnUrl: URL;
issuerUrl: URL; issuerUrl: URL;
}): URL { }): URL {
const { acsUrl, signOnUrl } = data; const { acsUrl, signOnUrl } = data;
const samlRequest: string = `<samlp:AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="${Text.generateRandomText( const samlRequest: string = `<samlp:AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="${Text.generateRandomText(
10 10,
).toUpperCase()}" Version="2.0" IssueInstant="${OneUptimeDate.getCurrentDate().toISOString()}" IsPassive="false" AssertionConsumerServiceURL="${acsUrl.toString()}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ForceAuthn="false"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">${data.issuerUrl.toString()}</Issuer></samlp:AuthnRequest>`; ).toUpperCase()}" Version="2.0" IssueInstant="${OneUptimeDate.getCurrentDate().toISOString()}" IsPassive="false" AssertionConsumerServiceURL="${acsUrl.toString()}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ForceAuthn="false"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">${data.issuerUrl.toString()}</Issuer></samlp:AuthnRequest>`;
const deflated: Buffer = zlib.deflateRawSync(samlRequest); const deflated: Buffer = zlib.deflateRawSync(samlRequest);
const base64Encoded: string = deflated.toString('base64'); const base64Encoded: string = deflated.toString("base64");
return URL.fromString(signOnUrl.toString()).addQueryParam( return URL.fromString(signOnUrl.toString()).addQueryParam(
'SAMLRequest', "SAMLRequest",
base64Encoded, base64Encoded,
true true,
); );
}
public static isPayloadValid(payload: JSONObject): void {
if (
!payload["saml2p:Response"] &&
!payload["samlp:Response"] &&
!payload["samlp:Response"]
) {
throw new BadRequestException("SAML Response not found.");
} }
public static isPayloadValid(payload: JSONObject): void { payload =
if ( (payload["saml2p:Response"] as JSONObject) ||
!payload['saml2p:Response'] && (payload["samlp:Response"] as JSONObject) ||
!payload['samlp:Response'] && (payload["samlp:Response"] as JSONObject) ||
!payload['samlp:Response'] (payload["Response"] as JSONObject);
) {
throw new BadRequestException('SAML Response not found.');
}
payload = const issuers: JSONArray =
(payload['saml2p:Response'] as JSONObject) || (payload["saml2:Issuer"] as JSONArray) ||
(payload['samlp:Response'] as JSONObject) || (payload["saml:Issuer"] as JSONArray) ||
(payload['samlp:Response'] as JSONObject) || (payload["Issuer"] as JSONArray);
(payload['Response'] as JSONObject);
const issuers: JSONArray = if (issuers.length === 0) {
(payload['saml2:Issuer'] as JSONArray) || throw new BadRequestException("Issuers not found");
(payload['saml:Issuer'] as JSONArray) ||
(payload['Issuer'] as JSONArray);
if (issuers.length === 0) {
throw new BadRequestException('Issuers not found');
}
const issuer: JSONObject | string | undefined = issuers[0];
if (typeof issuer === 'string') {
return issuer;
}
if (!issuer) {
throw new BadRequestException('Issuer not found');
}
const issuerUrl: string = issuer['_'] as string;
if (!issuerUrl) {
throw new BadRequestException(
'Issuer URL not found in SAML response'
);
}
const samlAssertion: JSONArray =
(payload['saml2:Assertion'] as JSONArray) ||
(payload['saml:Assertion'] as JSONArray) ||
(payload['Assertion'] as JSONArray);
if (!samlAssertion || samlAssertion.length === 0) {
throw new BadRequestException('SAML Assertion not found');
}
const samlSubject: JSONArray =
((samlAssertion[0] as JSONObject)['saml2:Subject'] as JSONArray) ||
((samlAssertion[0] as JSONObject)['saml:Subject'] as JSONArray) ||
((samlAssertion[0] as JSONObject)['Subject'] as JSONArray);
if (!samlSubject || samlSubject.length === 0) {
throw new BadRequestException('SAML Subject not found');
}
const samlNameId: JSONArray =
((samlSubject[0] as JSONObject)['saml2:NameID'] as JSONArray) ||
((samlSubject[0] as JSONObject)['saml:NameID'] as JSONArray) ||
((samlSubject[0] as JSONObject)['NameID'] as JSONArray);
if (!samlNameId || samlNameId.length === 0) {
throw new BadRequestException('SAML NAME ID not found');
}
const emailString: string = (samlNameId[0] as JSONObject)[
'_'
] as string;
if (!emailString) {
if (!samlNameId || samlNameId.length === 0) {
throw new BadRequestException('SAML Email not found');
}
}
} }
public static isSignatureValid( const issuer: JSONObject | string | undefined = issuers[0];
samlPayload: string,
certificate: string
): boolean {
try {
const dom: Document = new xmldom.DOMParser().parseFromString(
samlPayload
);
const signature: Element | undefined = dom.getElementsByTagNameNS(
'http://www.w3.org/2000/09/xmldsig#',
'Signature'
)[0];
const sig: xmlCrypto.SignedXml = new xmlCrypto.SignedXml();
sig.keyInfoProvider = { if (typeof issuer === "string") {
getKeyInfo: function (_key: any) { return issuer;
return `<X509Data><X509Certificate>${certificate}</X509Certificate></X509Data>`; }
}, if (!issuer) {
getKey: function () { throw new BadRequestException("Issuer not found");
return certificate;
} as any,
} as FileKeyInfo;
sig.loadSignature(signature!.toString());
const res: boolean = sig.checkSignature(samlPayload);
return res;
} catch (err) {
logger.error(err);
return false;
}
} }
public static getEmail(payload: JSONObject): Email { const issuerUrl: string = issuer["_"] as string;
if (!payload['saml2p:Response'] && !payload['samlp:Response']) {
throw new BadRequestException('SAML Response not found.');
}
payload = if (!issuerUrl) {
(payload['saml2p:Response'] as JSONObject) || throw new BadRequestException("Issuer URL not found in SAML response");
(payload['samlp:Response'] as JSONObject) ||
(payload['Response'] as JSONObject);
const samlAssertion: JSONArray =
(payload['saml2:Assertion'] as JSONArray) ||
(payload['saml:Assertion'] as JSONArray) ||
(payload['Assertion'] as JSONArray);
if (!samlAssertion || samlAssertion.length === 0) {
throw new BadRequestException('SAML Assertion not found');
}
const samlSubject: JSONArray =
((samlAssertion[0] as JSONObject)['saml2:Subject'] as JSONArray) ||
((samlAssertion[0] as JSONObject)['saml:Subject'] as JSONArray) ||
((samlAssertion[0] as JSONObject)['Subject'] as JSONArray);
if (!samlSubject || samlSubject.length === 0) {
throw new BadRequestException('SAML Subject not found');
}
const samlNameId: JSONArray =
((samlSubject[0] as JSONObject)['saml2:NameID'] as JSONArray) ||
((samlSubject[0] as JSONObject)['saml:NameID'] as JSONArray) ||
((samlSubject[0] as JSONObject)['NameID'] as JSONArray);
if (!samlNameId || samlNameId.length === 0) {
throw new BadRequestException('SAML NAME ID not found');
}
const emailString: string = (samlNameId[0] as JSONObject)[
'_'
] as string;
return new Email(emailString.trim());
} }
public static getIssuer(payload: JSONObject): string { const samlAssertion: JSONArray =
if (!payload['saml2p:Response'] && !payload['samlp:Response']) { (payload["saml2:Assertion"] as JSONArray) ||
throw new BadRequestException('SAML Response not found.'); (payload["saml:Assertion"] as JSONArray) ||
} (payload["Assertion"] as JSONArray);
payload = if (!samlAssertion || samlAssertion.length === 0) {
(payload['saml2p:Response'] as JSONObject) || throw new BadRequestException("SAML Assertion not found");
(payload['samlp:Response'] as JSONObject) ||
(payload['Response'] as JSONObject);
const issuers: JSONArray =
(payload['saml2:Issuer'] as JSONArray) ||
(payload['saml:Issuer'] as JSONArray) ||
(payload['Issuer'] as JSONArray);
if (issuers.length === 0) {
throw new BadRequestException('Issuers not found');
}
const issuer: JSONObject | string | undefined = issuers[0];
if (typeof issuer === 'string') {
return issuer;
}
if (!issuer) {
throw new BadRequestException('Issuer not found');
}
const issuerUrl: string = issuer['_'] as string;
if (!issuerUrl) {
throw new BadRequestException(
'Issuer URL not found in SAML response'
);
}
return issuerUrl.trim();
} }
const samlSubject: JSONArray =
((samlAssertion[0] as JSONObject)["saml2:Subject"] as JSONArray) ||
((samlAssertion[0] as JSONObject)["saml:Subject"] as JSONArray) ||
((samlAssertion[0] as JSONObject)["Subject"] as JSONArray);
if (!samlSubject || samlSubject.length === 0) {
throw new BadRequestException("SAML Subject not found");
}
const samlNameId: JSONArray =
((samlSubject[0] as JSONObject)["saml2:NameID"] as JSONArray) ||
((samlSubject[0] as JSONObject)["saml:NameID"] as JSONArray) ||
((samlSubject[0] as JSONObject)["NameID"] as JSONArray);
if (!samlNameId || samlNameId.length === 0) {
throw new BadRequestException("SAML NAME ID not found");
}
const emailString: string = (samlNameId[0] as JSONObject)["_"] as string;
if (!emailString) {
if (!samlNameId || samlNameId.length === 0) {
throw new BadRequestException("SAML Email not found");
}
}
}
public static isSignatureValid(
samlPayload: string,
certificate: string,
): boolean {
try {
const dom: Document = new xmldom.DOMParser().parseFromString(samlPayload);
const signature: Element | undefined = dom.getElementsByTagNameNS(
"http://www.w3.org/2000/09/xmldsig#",
"Signature",
)[0];
const sig: xmlCrypto.SignedXml = new xmlCrypto.SignedXml();
sig.keyInfoProvider = {
getKeyInfo: function (_key: any) {
return `<X509Data><X509Certificate>${certificate}</X509Certificate></X509Data>`;
},
getKey: function () {
return certificate;
} as any,
} as FileKeyInfo;
sig.loadSignature(signature!.toString());
const res: boolean = sig.checkSignature(samlPayload);
return res;
} catch (err) {
logger.error(err);
return false;
}
}
public static getEmail(payload: JSONObject): Email {
if (!payload["saml2p:Response"] && !payload["samlp:Response"]) {
throw new BadRequestException("SAML Response not found.");
}
payload =
(payload["saml2p:Response"] as JSONObject) ||
(payload["samlp:Response"] as JSONObject) ||
(payload["Response"] as JSONObject);
const samlAssertion: JSONArray =
(payload["saml2:Assertion"] as JSONArray) ||
(payload["saml:Assertion"] as JSONArray) ||
(payload["Assertion"] as JSONArray);
if (!samlAssertion || samlAssertion.length === 0) {
throw new BadRequestException("SAML Assertion not found");
}
const samlSubject: JSONArray =
((samlAssertion[0] as JSONObject)["saml2:Subject"] as JSONArray) ||
((samlAssertion[0] as JSONObject)["saml:Subject"] as JSONArray) ||
((samlAssertion[0] as JSONObject)["Subject"] as JSONArray);
if (!samlSubject || samlSubject.length === 0) {
throw new BadRequestException("SAML Subject not found");
}
const samlNameId: JSONArray =
((samlSubject[0] as JSONObject)["saml2:NameID"] as JSONArray) ||
((samlSubject[0] as JSONObject)["saml:NameID"] as JSONArray) ||
((samlSubject[0] as JSONObject)["NameID"] as JSONArray);
if (!samlNameId || samlNameId.length === 0) {
throw new BadRequestException("SAML NAME ID not found");
}
const emailString: string = (samlNameId[0] as JSONObject)["_"] as string;
return new Email(emailString.trim());
}
public static getIssuer(payload: JSONObject): string {
if (!payload["saml2p:Response"] && !payload["samlp:Response"]) {
throw new BadRequestException("SAML Response not found.");
}
payload =
(payload["saml2p:Response"] as JSONObject) ||
(payload["samlp:Response"] as JSONObject) ||
(payload["Response"] as JSONObject);
const issuers: JSONArray =
(payload["saml2:Issuer"] as JSONArray) ||
(payload["saml:Issuer"] as JSONArray) ||
(payload["Issuer"] as JSONArray);
if (issuers.length === 0) {
throw new BadRequestException("Issuers not found");
}
const issuer: JSONObject | string | undefined = issuers[0];
if (typeof issuer === "string") {
return issuer;
}
if (!issuer) {
throw new BadRequestException("Issuer not found");
}
const issuerUrl: string = issuer["_"] as string;
if (!issuerUrl) {
throw new BadRequestException("Issuer URL not found in SAML response");
}
return issuerUrl.trim();
}
} }

View File

@@ -1,144 +1,143 @@
import CallService from '../Services/CallService'; import CallService from "../Services/CallService";
import CallRequest from 'Common/Types/Call/CallRequest'; import CallRequest from "Common/Types/Call/CallRequest";
import TwilioConfig from 'Common/Types/CallAndSMS/TwilioConfig'; import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import JSONFunctions from 'Common/Types/JSONFunctions'; import JSONFunctions from "Common/Types/JSONFunctions";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import Phone from 'Common/Types/Phone'; import Phone from "Common/Types/Phone";
import ClusterKeyAuthorization from 'CommonServer/Middleware/ClusterKeyAuthorization'; import ClusterKeyAuthorization from "CommonServer/Middleware/ClusterKeyAuthorization";
import ProjectCallSMSConfigService from 'CommonServer/Services/ProjectCallSMSConfigService'; import ProjectCallSMSConfigService from "CommonServer/Services/ProjectCallSMSConfigService";
import Express, { import Express, {
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressRouter, ExpressRouter,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import Response from 'CommonServer/Utils/Response'; import Response from "CommonServer/Utils/Response";
import ProjectCallSMSConfig from 'Model/Models/ProjectCallSMSConfig'; import ProjectCallSMSConfig from "Model/Models/ProjectCallSMSConfig";
const router: ExpressRouter = Express.getRouter(); const router: ExpressRouter = Express.getRouter();
router.post( router.post(
'/make-call', "/make-call",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware, ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => { async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body); const body: JSONObject = JSONFunctions.deserialize(req.body);
await CallService.makeCall(body['callRequest'] as CallRequest, { await CallService.makeCall(body["callRequest"] as CallRequest, {
projectId: body['projectId'] as ObjectID, projectId: body["projectId"] as ObjectID,
isSensitive: (body['isSensitive'] as boolean) || false, isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId: userOnCallLogTimelineId:
(body['userOnCallLogTimelineId'] as ObjectID) || undefined, (body["userOnCallLogTimelineId"] as ObjectID) || undefined,
customTwilioConfig: body['customTwilioConfig'] as any, customTwilioConfig: body["customTwilioConfig"] as any,
}); });
return Response.sendEmptySuccessResponse(req, res);
}
);
router.post('/test', async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
const callSMSConfigId: ObjectID = new ObjectID(
body['callSMSConfigId'] as string
);
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPhoneNumber: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
'call and sms config not found for id' +
callSMSConfigId.toString()
)
);
}
const toPhone: Phone = new Phone(body['toPhone'] as string);
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('toPhone is required')
);
}
// if any of the twilio config is missing, we will not send make the call
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('twilioAccountSID is required')
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('twilioAuthToken is required')
);
}
if (!config.twilioPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('twilioPhoneNumber is required')
);
}
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
try {
if (!twilioConfig) {
throw new BadDataException('twilioConfig is undefined');
}
const testCallRequest: CallRequest = {
data: [
{
sayMessage: 'This is a test call from OneUptime.',
},
],
to: toPhone,
};
await CallService.makeCall(testCallRequest, {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
'Error making test call. Please check the twilio logs for more details'
)
);
}
return Response.sendEmptySuccessResponse(req, res); return Response.sendEmptySuccessResponse(req, res);
},
);
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPhoneNumber: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
const toPhone: Phone = new Phone(body["toPhone"] as string);
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
// if any of the twilio config is missing, we will not send make the call
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPhoneNumber is required"),
);
}
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
}
const testCallRequest: CallRequest = {
data: [
{
sayMessage: "This is a test call from OneUptime.",
},
],
to: toPhone,
};
await CallService.makeCall(testCallRequest, {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Error making test call. Please check the twilio logs for more details",
),
);
}
return Response.sendEmptySuccessResponse(req, res);
}); });
export default router; export default router;

View File

@@ -1,65 +1,65 @@
import MailService from '../Services/MailService'; import MailService from "../Services/MailService";
import Dictionary from 'Common/Types/Dictionary'; import Dictionary from "Common/Types/Dictionary";
import Email from 'Common/Types/Email'; import Email from "Common/Types/Email";
import EmailMessage from 'Common/Types/Email/EmailMessage'; import EmailMessage from "Common/Types/Email/EmailMessage";
import EmailServer from 'Common/Types/Email/EmailServer'; import EmailServer from "Common/Types/Email/EmailServer";
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import ClusterKeyAuthorization from 'CommonServer/Middleware/ClusterKeyAuthorization'; import ClusterKeyAuthorization from "CommonServer/Middleware/ClusterKeyAuthorization";
import Express, { import Express, {
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressRouter, ExpressRouter,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import Response from 'CommonServer/Utils/Response'; import Response from "CommonServer/Utils/Response";
const router: ExpressRouter = Express.getRouter(); const router: ExpressRouter = Express.getRouter();
router.post( router.post(
'/send', "/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware, ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => { async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body; const body: JSONObject = req.body;
const mail: EmailMessage = { const mail: EmailMessage = {
templateType: body['templateType'] as EmailTemplateType, templateType: body["templateType"] as EmailTemplateType,
toEmail: new Email(body['toEmail'] as string), toEmail: new Email(body["toEmail"] as string),
subject: body['subject'] as string, subject: body["subject"] as string,
vars: body['vars'] as Dictionary<string>, vars: body["vars"] as Dictionary<string>,
body: (body['body'] as string) || '', body: (body["body"] as string) || "",
}; };
let mailServer: EmailServer | undefined = undefined; let mailServer: EmailServer | undefined = undefined;
if (hasMailServerSettingsInBody(body)) { if (hasMailServerSettingsInBody(body)) {
mailServer = MailService.getEmailServer(req.body); mailServer = MailService.getEmailServer(req.body);
}
await MailService.send(mail, {
projectId: body['projectId']
? new ObjectID(body['projectId'] as string)
: undefined,
emailServer: mailServer,
userOnCallLogTimelineId:
(body['userOnCallLogTimelineId'] as ObjectID) || undefined,
});
return Response.sendEmptySuccessResponse(req, res);
} }
await MailService.send(mail, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
emailServer: mailServer,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
});
return Response.sendEmptySuccessResponse(req, res);
},
); );
type HasMailServerSettingsInBody = (body: JSONObject) => boolean; type HasMailServerSettingsInBody = (body: JSONObject) => boolean;
const hasMailServerSettingsInBody: HasMailServerSettingsInBody = ( const hasMailServerSettingsInBody: HasMailServerSettingsInBody = (
body: JSONObject body: JSONObject,
): boolean => { ): boolean => {
return ( return (
body && body &&
Object.keys(body).filter((key: string) => { Object.keys(body).filter((key: string) => {
return key.startsWith('SMTP_'); return key.startsWith("SMTP_");
}).length > 0 }).length > 0
); );
}; };
export default router; export default router;

View File

@@ -1,142 +1,133 @@
import SmsService from '../Services/SmsService'; import SmsService from "../Services/SmsService";
import TwilioConfig from 'Common/Types/CallAndSMS/TwilioConfig'; import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import JSONFunctions from 'Common/Types/JSONFunctions'; import JSONFunctions from "Common/Types/JSONFunctions";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import Phone from 'Common/Types/Phone'; import Phone from "Common/Types/Phone";
import ClusterKeyAuthorization from 'CommonServer/Middleware/ClusterKeyAuthorization'; import ClusterKeyAuthorization from "CommonServer/Middleware/ClusterKeyAuthorization";
import ProjectCallSMSConfigService from 'CommonServer/Services/ProjectCallSMSConfigService'; import ProjectCallSMSConfigService from "CommonServer/Services/ProjectCallSMSConfigService";
import Express, { import Express, {
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressRouter, ExpressRouter,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import Response from 'CommonServer/Utils/Response'; import Response from "CommonServer/Utils/Response";
import ProjectCallSMSConfig from 'Model/Models/ProjectCallSMSConfig'; import ProjectCallSMSConfig from "Model/Models/ProjectCallSMSConfig";
const router: ExpressRouter = Express.getRouter(); const router: ExpressRouter = Express.getRouter();
router.post( router.post(
'/send', "/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware, ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => { async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body); const body: JSONObject = JSONFunctions.deserialize(req.body);
await SmsService.sendSms( await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
body['to'] as Phone, projectId: body["projectId"] as ObjectID,
body['message'] as string, isSensitive: (body["isSensitive"] as boolean) || false,
{ userOnCallLogTimelineId:
projectId: body['projectId'] as ObjectID, (body["userOnCallLogTimelineId"] as ObjectID) || undefined,
isSensitive: (body['isSensitive'] as boolean) || false, customTwilioConfig: body["customTwilioConfig"] as any,
userOnCallLogTimelineId: });
(body['userOnCallLogTimelineId'] as ObjectID) || undefined,
customTwilioConfig: body['customTwilioConfig'] as any,
}
);
return Response.sendEmptySuccessResponse(req, res);
}
);
router.post('/test', async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
const callSMSConfigId: ObjectID = new ObjectID(
body['callSMSConfigId'] as string
);
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPhoneNumber: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
'call and sms config not found for id' +
callSMSConfigId.toString()
)
);
}
const toPhone: Phone = new Phone(body['toPhone'] as string);
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('toPhone is required')
);
}
// if any of the twilio config is missing, we will not send make the call
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('twilioAccountSID is required')
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('twilioAuthToken is required')
);
}
if (!config.twilioPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('twilioPhoneNumber is required')
);
}
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
try {
if (!twilioConfig) {
throw new BadDataException('twilioConfig is undefined');
}
await SmsService.sendSms(
toPhone,
'This is a test SMS from OneUptime.',
{
projectId: config.projectId,
customTwilioConfig: twilioConfig,
}
);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
'Failed to send test SMS. Please check the twilio logs for more details.'
)
);
}
return Response.sendEmptySuccessResponse(req, res); return Response.sendEmptySuccessResponse(req, res);
},
);
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPhoneNumber: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
const toPhone: Phone = new Phone(body["toPhone"] as string);
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
// if any of the twilio config is missing, we will not send make the call
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPhoneNumber is required"),
);
}
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
}
await SmsService.sendSms(toPhone, "This is a test SMS from OneUptime.", {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Failed to send test SMS. Please check the twilio logs for more details.",
),
);
}
return Response.sendEmptySuccessResponse(req, res);
}); });
export default router; export default router;

View File

@@ -1,104 +1,104 @@
import MailService from '../Services/MailService'; import MailService from "../Services/MailService";
import Email from 'Common/Types/Email'; import Email from "Common/Types/Email";
import EmailMessage from 'Common/Types/Email/EmailMessage'; import EmailMessage from "Common/Types/Email/EmailMessage";
import EmailServer from 'Common/Types/Email/EmailServer'; import EmailServer from "Common/Types/Email/EmailServer";
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONObject } from 'Common/Types/JSON'; import { JSONObject } from "Common/Types/JSON";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import ProjectSMTPConfigService from 'CommonServer/Services/ProjectSmtpConfigService'; import ProjectSMTPConfigService from "CommonServer/Services/ProjectSmtpConfigService";
import Express, { import Express, {
ExpressRequest, ExpressRequest,
ExpressResponse, ExpressResponse,
ExpressRouter, ExpressRouter,
} from 'CommonServer/Utils/Express'; } from "CommonServer/Utils/Express";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import Response from 'CommonServer/Utils/Response'; import Response from "CommonServer/Utils/Response";
import ProjectSmtpConfig from 'Model/Models/ProjectSmtpConfig'; import ProjectSmtpConfig from "Model/Models/ProjectSmtpConfig";
const router: ExpressRouter = Express.getRouter(); const router: ExpressRouter = Express.getRouter();
router.post('/test', async (req: ExpressRequest, res: ExpressResponse) => { router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body; const body: JSONObject = req.body;
const smtpConfigId: ObjectID = new ObjectID(body['smtpConfigId'] as string); const smtpConfigId: ObjectID = new ObjectID(body["smtpConfigId"] as string);
const config: ProjectSmtpConfig | null = const config: ProjectSmtpConfig | null =
await ProjectSMTPConfigService.findOneById({ await ProjectSMTPConfigService.findOneById({
id: smtpConfigId, id: smtpConfigId,
props: { props: {
isRoot: true, isRoot: true,
}, },
select: { select: {
_id: true, _id: true,
hostname: true, hostname: true,
port: true, port: true,
username: true, username: true,
password: true, password: true,
fromEmail: true, fromEmail: true,
fromName: true, fromName: true,
secure: true, secure: true,
projectId: true, projectId: true,
}, },
}); });
if (!config) { if (!config) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadDataException( new BadDataException(
'smtp-config not found for id' + smtpConfigId.toString() "smtp-config not found for id" + smtpConfigId.toString(),
) ),
); );
} }
const toEmail: Email = new Email(body['toEmail'] as string); const toEmail: Email = new Email(body["toEmail"] as string);
if (!toEmail) { if (!toEmail) {
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadDataException('toEmail is required') new BadDataException("toEmail is required"),
); );
} }
const mail: EmailMessage = { const mail: EmailMessage = {
templateType: EmailTemplateType.SMTPTest, templateType: EmailTemplateType.SMTPTest,
toEmail: new Email(body['toEmail'] as string), toEmail: new Email(body["toEmail"] as string),
subject: 'Test Email from OneUptime', subject: "Test Email from OneUptime",
vars: {}, vars: {},
body: '', body: "",
}; };
const mailServer: EmailServer = { const mailServer: EmailServer = {
id: config.id!, id: config.id!,
host: config.hostname!, host: config.hostname!,
port: config.port!, port: config.port!,
username: config.username!, username: config.username!,
password: config.password!, password: config.password!,
fromEmail: config.fromEmail!, fromEmail: config.fromEmail!,
fromName: config.fromName!, fromName: config.fromName!,
secure: Boolean(config.secure), secure: Boolean(config.secure),
}; };
try { try {
await MailService.send(mail, { await MailService.send(mail, {
emailServer: mailServer, emailServer: mailServer,
projectId: config.projectId!, projectId: config.projectId!,
timeout: 4000, timeout: 4000,
}); });
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
return Response.sendErrorResponse( return Response.sendErrorResponse(
req, req,
res, res,
new BadDataException( new BadDataException(
'Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.' "Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
) ),
); );
} }
return Response.sendEmptySuccessResponse(req, res); return Response.sendEmptySuccessResponse(req, res);
}); });
export default router; export default router;

View File

@@ -1,18 +1,18 @@
import Hostname from 'Common/Types/API/Hostname'; import Hostname from "Common/Types/API/Hostname";
import TwilioConfig from 'Common/Types/CallAndSMS/TwilioConfig'; import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
import Email from 'Common/Types/Email'; import Email from "Common/Types/Email";
import EmailServer from 'Common/Types/Email/EmailServer'; import EmailServer from "Common/Types/Email/EmailServer";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import Port from 'Common/Types/Port'; import Port from "Common/Types/Port";
import GlobalConfigService from 'CommonServer/Services/GlobalConfigService'; import GlobalConfigService from "CommonServer/Services/GlobalConfigService";
import GlobalConfig, { EmailServerType } from 'Model/Models/GlobalConfig'; import GlobalConfig, { EmailServerType } from "Model/Models/GlobalConfig";
export const InternalSmtpPassword: string = export const InternalSmtpPassword: string =
process.env['INTERNAL_SMTP_PASSWORD'] || ''; process.env["INTERNAL_SMTP_PASSWORD"] || "";
export const InternalSmtpHost: Hostname = new Hostname( export const InternalSmtpHost: Hostname = new Hostname(
process.env['INTERNAL_SMTP_HOST'] || 'haraka' process.env["INTERNAL_SMTP_HOST"] || "haraka",
); );
export const InternalSmtpPort: Port = new Port(2525); export const InternalSmtpPort: Port = new Port(2525);
@@ -20,187 +20,187 @@ export const InternalSmtpPort: Port = new Port(2525);
export const InternalSmtpSecure: boolean = false; export const InternalSmtpSecure: boolean = false;
export const InternalSmtpEmail: Email = new Email( export const InternalSmtpEmail: Email = new Email(
process.env['INTERNAL_SMTP_EMAIL'] || 'noreply@oneuptime.com' process.env["INTERNAL_SMTP_EMAIL"] || "noreply@oneuptime.com",
); );
export const InternalSmtpFromName: string = export const InternalSmtpFromName: string =
process.env['INTERNAL_SMTP_FROM_NAME'] || 'OneUptime'; process.env["INTERNAL_SMTP_FROM_NAME"] || "OneUptime";
type GetGlobalSMTPConfig = () => Promise<EmailServer | null>; type GetGlobalSMTPConfig = () => Promise<EmailServer | null>;
export const getGlobalSMTPConfig: GetGlobalSMTPConfig = export const getGlobalSMTPConfig: GetGlobalSMTPConfig =
async (): Promise<EmailServer | null> => { async (): Promise<EmailServer | null> => {
const globalConfig: GlobalConfig | null = const globalConfig: GlobalConfig | null =
await GlobalConfigService.findOneBy({ await GlobalConfigService.findOneBy({
query: { query: {
_id: ObjectID.getZeroObjectID().toString(), _id: ObjectID.getZeroObjectID().toString(),
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
select: { select: {
smtpFromEmail: true, smtpFromEmail: true,
smtpHost: true, smtpHost: true,
smtpPort: true, smtpPort: true,
smtpUsername: true, smtpUsername: true,
smtpPassword: true, smtpPassword: true,
isSMTPSecure: true, isSMTPSecure: true,
smtpFromName: true, smtpFromName: true,
}, },
}); });
if (!globalConfig) { if (!globalConfig) {
throw new BadDataException('Global Config not found'); throw new BadDataException("Global Config not found");
} }
if ( if (
!globalConfig.smtpFromEmail || !globalConfig.smtpFromEmail ||
!globalConfig.smtpHost || !globalConfig.smtpHost ||
!globalConfig.smtpPort || !globalConfig.smtpPort ||
!globalConfig.smtpUsername || !globalConfig.smtpUsername ||
!globalConfig.smtpPassword || !globalConfig.smtpPassword ||
!globalConfig.smtpFromName !globalConfig.smtpFromName
) { ) {
return null; return null;
} }
return { return {
host: globalConfig.smtpHost, host: globalConfig.smtpHost,
port: globalConfig.smtpPort, port: globalConfig.smtpPort,
username: globalConfig.smtpUsername, username: globalConfig.smtpUsername,
password: globalConfig.smtpPassword, password: globalConfig.smtpPassword,
secure: globalConfig.isSMTPSecure || false, secure: globalConfig.isSMTPSecure || false,
fromEmail: globalConfig.smtpFromEmail, fromEmail: globalConfig.smtpFromEmail,
fromName: globalConfig.smtpFromName, fromName: globalConfig.smtpFromName,
};
}; };
};
type GetEmailServerTypeFunction = () => Promise<EmailServerType>; type GetEmailServerTypeFunction = () => Promise<EmailServerType>;
export const getEmailServerType: GetEmailServerTypeFunction = export const getEmailServerType: GetEmailServerTypeFunction =
async (): Promise<EmailServerType> => { async (): Promise<EmailServerType> => {
const globalConfig: GlobalConfig | null = const globalConfig: GlobalConfig | null =
await GlobalConfigService.findOneBy({ await GlobalConfigService.findOneBy({
query: { query: {
_id: ObjectID.getZeroObjectID().toString(), _id: ObjectID.getZeroObjectID().toString(),
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
select: { select: {
emailServerType: true, emailServerType: true,
}, },
}); });
if (!globalConfig) { if (!globalConfig) {
return EmailServerType.Internal; return EmailServerType.Internal;
} }
return globalConfig.emailServerType || EmailServerType.Internal; return globalConfig.emailServerType || EmailServerType.Internal;
}; };
export interface SendGridConfig { export interface SendGridConfig {
apiKey: string; apiKey: string;
fromName: string; fromName: string;
fromEmail: Email; fromEmail: Email;
} }
type GetSendgridConfigFunction = () => Promise<SendGridConfig | null>; type GetSendgridConfigFunction = () => Promise<SendGridConfig | null>;
export const getSendgridConfig: GetSendgridConfigFunction = export const getSendgridConfig: GetSendgridConfigFunction =
async (): Promise<SendGridConfig | null> => { async (): Promise<SendGridConfig | null> => {
const globalConfig: GlobalConfig | null = const globalConfig: GlobalConfig | null =
await GlobalConfigService.findOneBy({ await GlobalConfigService.findOneBy({
query: { query: {
_id: ObjectID.getZeroObjectID().toString(), _id: ObjectID.getZeroObjectID().toString(),
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
select: { select: {
sendgridApiKey: true, sendgridApiKey: true,
sendgridFromEmail: true, sendgridFromEmail: true,
sendgridFromName: true, sendgridFromName: true,
}, },
}); });
if (!globalConfig) { if (!globalConfig) {
return null; return null;
} }
if ( if (
globalConfig.sendgridApiKey && globalConfig.sendgridApiKey &&
globalConfig.sendgridFromEmail && globalConfig.sendgridFromEmail &&
globalConfig.sendgridFromName globalConfig.sendgridFromName
) { ) {
return { return {
apiKey: globalConfig.sendgridApiKey, apiKey: globalConfig.sendgridApiKey,
fromName: globalConfig.sendgridFromName, fromName: globalConfig.sendgridFromName,
fromEmail: globalConfig.sendgridFromEmail, fromEmail: globalConfig.sendgridFromEmail,
}; };
} }
return null; return null;
}; };
type GetTwilioConfigFunction = () => Promise<TwilioConfig | null>; type GetTwilioConfigFunction = () => Promise<TwilioConfig | null>;
export const getTwilioConfig: GetTwilioConfigFunction = export const getTwilioConfig: GetTwilioConfigFunction =
async (): Promise<TwilioConfig | null> => { async (): Promise<TwilioConfig | null> => {
const globalConfig: GlobalConfig | null = const globalConfig: GlobalConfig | null =
await GlobalConfigService.findOneBy({ await GlobalConfigService.findOneBy({
query: { query: {
_id: ObjectID.getZeroObjectID().toString(), _id: ObjectID.getZeroObjectID().toString(),
}, },
props: { props: {
isRoot: true, isRoot: true,
}, },
select: { select: {
twilioAccountSID: true, twilioAccountSID: true,
twilioAuthToken: true, twilioAuthToken: true,
twilioPhoneNumber: true, twilioPhoneNumber: true,
}, },
}); });
if (!globalConfig) { if (!globalConfig) {
throw new BadDataException('Global Config not found'); throw new BadDataException("Global Config not found");
} }
if ( if (
!globalConfig.twilioAccountSID || !globalConfig.twilioAccountSID ||
!globalConfig.twilioAuthToken || !globalConfig.twilioAuthToken ||
!globalConfig.twilioPhoneNumber !globalConfig.twilioPhoneNumber
) { ) {
return null; return null;
} }
return { return {
accountSid: globalConfig.twilioAccountSID, accountSid: globalConfig.twilioAccountSID,
authToken: globalConfig.twilioAuthToken, authToken: globalConfig.twilioAuthToken,
phoneNumber: globalConfig.twilioPhoneNumber, phoneNumber: globalConfig.twilioPhoneNumber,
};
}; };
};
export const SMSDefaultCostInCents: number = process.env[ export const SMSDefaultCostInCents: number = process.env[
'SMS_DEFAULT_COST_IN_CENTS' "SMS_DEFAULT_COST_IN_CENTS"
] ]
? parseInt(process.env['SMS_DEFAULT_COST_IN_CENTS']) ? parseInt(process.env["SMS_DEFAULT_COST_IN_CENTS"])
: 0; : 0;
export const SMSHighRiskCostInCents: number = process.env[ export const SMSHighRiskCostInCents: number = process.env[
'SMS_HIGH_RISK_COST_IN_CENTS' "SMS_HIGH_RISK_COST_IN_CENTS"
] ]
? parseInt(process.env['SMS_HIGH_RISK_COST_IN_CENTS']) ? parseInt(process.env["SMS_HIGH_RISK_COST_IN_CENTS"])
: 0; : 0;
export const CallHighRiskCostInCentsPerMinute: number = process.env[ export const CallHighRiskCostInCentsPerMinute: number = process.env[
'CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE' "CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE"
] ]
? parseInt(process.env['CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE']) ? parseInt(process.env["CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE"])
: 0; : 0;
export const CallDefaultCostInCentsPerMinute: number = process.env[ export const CallDefaultCostInCentsPerMinute: number = process.env[
'CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE' "CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE"
] ]
? parseInt(process.env['CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE']) ? parseInt(process.env["CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE"])
: 0; : 0;

View File

@@ -1,23 +1,23 @@
import CallAPI from './API/Call'; import CallAPI from "./API/Call";
// API // API
import MailAPI from './API/Mail'; import MailAPI from "./API/Mail";
import SmsAPI from './API/SMS'; import SmsAPI from "./API/SMS";
import SMTPConfigAPI from './API/SMTPConfig'; import SMTPConfigAPI from "./API/SMTPConfig";
import './Utils/Handlebars'; import "./Utils/Handlebars";
import FeatureSet from 'CommonServer/Types/FeatureSet'; import FeatureSet from "CommonServer/Types/FeatureSet";
import Express, { ExpressApplication } from 'CommonServer/Utils/Express'; import Express, { ExpressApplication } from "CommonServer/Utils/Express";
import 'ejs'; import "ejs";
const NotificationFeatureSet: FeatureSet = { const NotificationFeatureSet: FeatureSet = {
init: async (): Promise<void> => { init: async (): Promise<void> => {
const APP_NAME: string = 'api/notification'; const APP_NAME: string = "api/notification";
const app: ExpressApplication = Express.getExpressApp(); const app: ExpressApplication = Express.getExpressApp();
app.use([`/${APP_NAME}/email`, '/email'], MailAPI); app.use([`/${APP_NAME}/email`, "/email"], MailAPI);
app.use([`/${APP_NAME}/sms`, '/sms'], SmsAPI); app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
app.use([`/${APP_NAME}/call`, '/call'], CallAPI); app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
app.use([`/${APP_NAME}/smtp-config`, '/smtp-config'], SMTPConfigAPI); app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
}, },
}; };
export default NotificationFeatureSet; export default NotificationFeatureSet;

View File

@@ -1,383 +1,364 @@
import { import {
CallDefaultCostInCentsPerMinute, CallDefaultCostInCentsPerMinute,
CallHighRiskCostInCentsPerMinute, CallHighRiskCostInCentsPerMinute,
getTwilioConfig, getTwilioConfig,
} from '../Config'; } from "../Config";
import CallRequest, { import CallRequest, {
GatherInput, GatherInput,
Say, Say,
isHighRiskPhoneNumber, isHighRiskPhoneNumber,
} from 'Common/Types/Call/CallRequest'; } from "Common/Types/Call/CallRequest";
import CallStatus from 'Common/Types/Call/CallStatus'; import CallStatus from "Common/Types/Call/CallStatus";
import TwilioConfig from 'Common/Types/CallAndSMS/TwilioConfig'; import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
import OneUptimeDate from 'Common/Types/Date'; import OneUptimeDate from "Common/Types/Date";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import JSONFunctions from 'Common/Types/JSONFunctions'; import JSONFunctions from "Common/Types/JSONFunctions";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import UserNotificationStatus from 'Common/Types/UserNotification/UserNotificationStatus'; import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus";
import { IsBillingEnabled } from 'CommonServer/EnvironmentConfig'; import { IsBillingEnabled } from "CommonServer/EnvironmentConfig";
import CallLogService from 'CommonServer/Services/CallLogService'; import CallLogService from "CommonServer/Services/CallLogService";
import NotificationService from 'CommonServer/Services/NotificationService'; import NotificationService from "CommonServer/Services/NotificationService";
import ProjectService from 'CommonServer/Services/ProjectService'; import ProjectService from "CommonServer/Services/ProjectService";
import UserOnCallLogTimelineService from 'CommonServer/Services/UserOnCallLogTimelineService'; import UserOnCallLogTimelineService from "CommonServer/Services/UserOnCallLogTimelineService";
import JSONWebToken from 'CommonServer/Utils/JsonWebToken'; import JSONWebToken from "CommonServer/Utils/JsonWebToken";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import CallLog from 'Model/Models/CallLog'; import CallLog from "Model/Models/CallLog";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
import Twilio from 'twilio'; import Twilio from "twilio";
import { CallInstance } from 'twilio/lib/rest/api/v2010/account/call'; import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
export default class CallService { export default class CallService {
public static async makeCall( public static async makeCall(
callRequest: CallRequest, callRequest: CallRequest,
options: { options: {
projectId?: ObjectID | undefined; // project id for sms log projectId?: ObjectID | undefined; // project id for sms log
isSensitive?: boolean; // if true, message will not be logged isSensitive?: boolean; // if true, message will not be logged
userOnCallLogTimelineId?: ObjectID | undefined; // user notification log timeline id userOnCallLogTimelineId?: ObjectID | undefined; // user notification log timeline id
customTwilioConfig?: TwilioConfig | undefined; customTwilioConfig?: TwilioConfig | undefined;
},
): Promise<void> {
let callError: Error | null = null;
const callLog: CallLog = new CallLog();
try {
logger.debug("Call Request received.");
let callCost: number = 0;
// is no custom twilio config is provided, use default twilio config and charge for call.
const shouldChargeForCall: boolean =
IsBillingEnabled && !options.customTwilioConfig;
if (shouldChargeForCall) {
callCost = CallDefaultCostInCentsPerMinute / 100;
if (isHighRiskPhoneNumber(callRequest.to)) {
callCost = CallHighRiskCostInCentsPerMinute / 100;
} }
): Promise<void> { }
let callError: Error | null = null;
const callLog: CallLog = new CallLog();
try { logger.debug("Call Cost: " + callCost);
logger.debug('Call Request received.');
let callCost: number = 0; const twilioConfig: TwilioConfig | null =
options.customTwilioConfig || (await getTwilioConfig());
// is no custom twilio config is provided, use default twilio config and charge for call. if (!twilioConfig) {
const shouldChargeForCall: boolean = throw new BadDataException("Twilio Config not found");
IsBillingEnabled && !options.customTwilioConfig; }
if (shouldChargeForCall) { const client: Twilio.Twilio = Twilio(
callCost = CallDefaultCostInCentsPerMinute / 100; twilioConfig.accountSid,
if (isHighRiskPhoneNumber(callRequest.to)) { twilioConfig.authToken,
callCost = CallHighRiskCostInCentsPerMinute / 100; );
}
}
logger.debug('Call Cost: ' + callCost); callLog.toNumber = callRequest.to;
callLog.fromNumber = twilioConfig.phoneNumber;
callLog.callData =
options && options.isSensitive
? { message: "This call is sensitive and is not logged" }
: JSON.parse(JSON.stringify(callRequest));
callLog.callCostInUSDCents = 0;
const twilioConfig: TwilioConfig | null = if (options.projectId) {
options.customTwilioConfig || (await getTwilioConfig()); callLog.projectId = options.projectId;
}
if (!twilioConfig) { let project: Project | null = null;
throw new BadDataException('Twilio Config not found');
}
const client: Twilio.Twilio = Twilio( // make sure project has enough balance.
twilioConfig.accountSid,
twilioConfig.authToken if (options.projectId) {
project = await ProjectService.findOneById({
id: options.projectId,
select: {
smsOrCallCurrentBalanceInUSDCents: true,
enableCallNotifications: true,
lowCallAndSMSBalanceNotificationSentToOwners: true,
name: true,
notEnabledSmsOrCallNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
logger.debug("Project found.");
if (!project) {
callLog.status = CallStatus.Error;
callLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
logger.error(callLog.statusMessage);
await CallLogService.create({
data: callLog,
props: {
isRoot: true,
},
});
return;
}
if (!project.enableCallNotifications) {
callLog.status = CallStatus.Error;
callLog.statusMessage = `Call notifications are not enabled for this project. Please enable Call notifications in Project Settings.`;
logger.error(callLog.statusMessage);
await CallLogService.create({
data: callLog,
props: {
isRoot: true,
},
});
if (!project.notEnabledSmsOrCallNotificationSentToOwners) {
await ProjectService.updateOneById({
data: {
notEnabledSmsOrCallNotificationSentToOwners: true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
"Call notifications not enabled for " + (project.name || ""),
`We tried to make a call to ${callRequest.to.toString()}. <br/> <br/> This Call was not sent because call notifications are not enabled for this project. Please enable call notifications in Project Settings.`,
); );
}
return;
}
callLog.toNumber = callRequest.to; if (shouldChargeForCall) {
callLog.fromNumber = twilioConfig.phoneNumber; // check if auto recharge is enabled and current balance is low.
callLog.callData = let updatedBalance: number =
options && options.isSensitive project.smsOrCallCurrentBalanceInUSDCents!;
? { message: 'This call is sensitive and is not logged' } try {
: JSON.parse(JSON.stringify(callRequest)); updatedBalance = await NotificationService.rechargeIfBalanceIsLow(
callLog.callCostInUSDCents = 0; project.id!,
);
} catch (err) {
logger.error(err);
}
if (options.projectId) { project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
callLog.projectId = options.projectId;
}
let project: Project | null = null; if (!project.smsOrCallCurrentBalanceInUSDCents) {
callLog.status = CallStatus.LowBalance;
// make sure project has enough balance. callLog.statusMessage = `Project ${options.projectId.toString()} does not have enough Call balance.`;
if (options.projectId) {
project = await ProjectService.findOneById({
id: options.projectId,
select: {
smsOrCallCurrentBalanceInUSDCents: true,
enableCallNotifications: true,
lowCallAndSMSBalanceNotificationSentToOwners: true,
name: true,
notEnabledSmsOrCallNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
logger.debug('Project found.');
if (!project) {
callLog.status = CallStatus.Error;
callLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
logger.error(callLog.statusMessage);
await CallLogService.create({
data: callLog,
props: {
isRoot: true,
},
});
return;
}
if (!project.enableCallNotifications) {
callLog.status = CallStatus.Error;
callLog.statusMessage = `Call notifications are not enabled for this project. Please enable Call notifications in Project Settings.`;
logger.error(callLog.statusMessage);
await CallLogService.create({
data: callLog,
props: {
isRoot: true,
},
});
if (!project.notEnabledSmsOrCallNotificationSentToOwners) {
await ProjectService.updateOneById({
data: {
notEnabledSmsOrCallNotificationSentToOwners:
true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
'Call notifications not enabled for ' +
(project.name || ''),
`We tried to make a call to ${callRequest.to.toString()}. <br/> <br/> This Call was not sent because call notifications are not enabled for this project. Please enable call notifications in Project Settings.`
);
}
return;
}
if (shouldChargeForCall) {
// check if auto recharge is enabled and current balance is low.
let updatedBalance: number =
project.smsOrCallCurrentBalanceInUSDCents!;
try {
updatedBalance =
await NotificationService.rechargeIfBalanceIsLow(
project.id!
);
} catch (err) {
logger.error(err);
}
project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
if (!project.smsOrCallCurrentBalanceInUSDCents) {
callLog.status = CallStatus.LowBalance;
callLog.statusMessage = `Project ${options.projectId.toString()} does not have enough Call balance.`;
logger.error(callLog.statusMessage);
await CallLogService.create({
data: callLog,
props: {
isRoot: true,
},
});
if (
!project.lowCallAndSMSBalanceNotificationSentToOwners
) {
await ProjectService.updateOneById({
data: {
lowCallAndSMSBalanceNotificationSentToOwners:
true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
'Low SMS and Call Balance for ' +
(project.name || ''),
`We tried to make a call to ${callRequest.to.toString()}. This call was not made because project does not have enough balance to make calls. Current balance is ${
(project.smsOrCallCurrentBalanceInUSDCents ||
0) / 100
} USD. Required balance to send this SMS should is ${callCost} USD. Please enable auto recharge or recharge manually.`
);
}
return;
}
if (
project.smsOrCallCurrentBalanceInUSDCents <
callCost * 100
) {
callLog.status = CallStatus.LowBalance;
callLog.statusMessage = `Project does not have enough balance to make this call. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${callCost} USD to make this call.`;
logger.error(callLog.statusMessage);
await CallLogService.create({
data: callLog,
props: {
isRoot: true,
},
});
if (
!project.lowCallAndSMSBalanceNotificationSentToOwners
) {
await ProjectService.updateOneById({
data: {
lowCallAndSMSBalanceNotificationSentToOwners:
true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
'Low SMS and Call Balance for ' +
(project.name || ''),
`We tried to make a call to ${callRequest.to.toString()}. This call was not made because project does not have enough balance to make a call. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents /
100
} USD. Required balance is ${callCost} USD to make this call. Please enable auto recharge or recharge manually.`
);
}
return;
}
}
}
logger.debug('Sending Call Request.');
const twillioCall: CallInstance = await client.calls.create({
twiml: this.generateTwimlForCall(callRequest),
to: callRequest.to.toString(),
from: twilioConfig.phoneNumber.toString(), // From a valid Twilio number
});
logger.debug('Call Request sent successfully.');
callLog.status = CallStatus.Success;
callLog.statusMessage = 'Call ID: ' + twillioCall.sid;
logger.debug('Call ID: ' + twillioCall.sid);
logger.debug(callLog.statusMessage);
if (shouldChargeForCall && project) {
logger.debug('Updating Project Balance.');
callLog.callCostInUSDCents = callCost * 100;
if (twillioCall && parseInt(twillioCall.duration) > 60) {
callLog.callCostInUSDCents = Math.ceil(
Math.ceil(parseInt(twillioCall.duration) / 60) *
(callCost * 100)
);
}
logger.debug('Call Cost: ' + callLog.callCostInUSDCents);
project.smsOrCallCurrentBalanceInUSDCents = Math.floor(
project.smsOrCallCurrentBalanceInUSDCents! - callCost * 100
);
await ProjectService.updateOneById({
data: {
smsOrCallCurrentBalanceInUSDCents:
project.smsOrCallCurrentBalanceInUSDCents,
notEnabledSmsOrCallNotificationSentToOwners: false, // reset this flag
},
id: project.id!,
props: {
isRoot: true,
},
});
logger.debug("Project's current balance updated.");
logger.debug(
'Current Balance: ' +
project.smsOrCallCurrentBalanceInUSDCents
);
}
} catch (e: any) {
callLog.callCostInUSDCents = 0;
callLog.status = CallStatus.Error;
callLog.statusMessage =
e && e.message ? e.message.toString() : e.toString();
logger.error('Call Request failed.');
logger.error(callLog.statusMessage); logger.error(callLog.statusMessage);
callError = e;
}
logger.debug('Saving Call Log if project id is provided.');
if (options.projectId) {
logger.debug('Saving Call Log.');
await CallLogService.create({ await CallLogService.create({
data: callLog, data: callLog,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
logger.debug('Call Log saved.');
} else {
logger.debug('Project Id is not provided. Call Log not saved.');
}
if (options.userOnCallLogTimelineId) { if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
await UserOnCallLogTimelineService.updateOneById({ await ProjectService.updateOneById({
data: { data: {
status: lowCallAndSMSBalanceNotificationSentToOwners: true,
callLog.status === CallStatus.Success
? UserNotificationStatus.Sent
: UserNotificationStatus.Error,
statusMessage: callLog.statusMessage!,
}, },
id: options.userOnCallLogTimelineId, id: project.id!,
props: { props: {
isRoot: true, isRoot: true,
}, },
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
"Low SMS and Call Balance for " + (project.name || ""),
`We tried to make a call to ${callRequest.to.toString()}. This call was not made because project does not have enough balance to make calls. Current balance is ${
(project.smsOrCallCurrentBalanceInUSDCents || 0) / 100
} USD. Required balance to send this SMS should is ${callCost} USD. Please enable auto recharge or recharge manually.`,
);
}
return;
}
if (project.smsOrCallCurrentBalanceInUSDCents < callCost * 100) {
callLog.status = CallStatus.LowBalance;
callLog.statusMessage = `Project does not have enough balance to make this call. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${callCost} USD to make this call.`;
logger.error(callLog.statusMessage);
await CallLogService.create({
data: callLog,
props: {
isRoot: true,
},
}); });
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
await ProjectService.updateOneById({
data: {
lowCallAndSMSBalanceNotificationSentToOwners: true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
"Low SMS and Call Balance for " + (project.name || ""),
`We tried to make a call to ${callRequest.to.toString()}. This call was not made because project does not have enough balance to make a call. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${callCost} USD to make this call. Please enable auto recharge or recharge manually.`,
);
}
return;
}
}
}
logger.debug("Sending Call Request.");
const twillioCall: CallInstance = await client.calls.create({
twiml: this.generateTwimlForCall(callRequest),
to: callRequest.to.toString(),
from: twilioConfig.phoneNumber.toString(), // From a valid Twilio number
});
logger.debug("Call Request sent successfully.");
callLog.status = CallStatus.Success;
callLog.statusMessage = "Call ID: " + twillioCall.sid;
logger.debug("Call ID: " + twillioCall.sid);
logger.debug(callLog.statusMessage);
if (shouldChargeForCall && project) {
logger.debug("Updating Project Balance.");
callLog.callCostInUSDCents = callCost * 100;
if (twillioCall && parseInt(twillioCall.duration) > 60) {
callLog.callCostInUSDCents = Math.ceil(
Math.ceil(parseInt(twillioCall.duration) / 60) * (callCost * 100),
);
} }
if (callError) { logger.debug("Call Cost: " + callLog.callCostInUSDCents);
throw callError;
} project.smsOrCallCurrentBalanceInUSDCents = Math.floor(
project.smsOrCallCurrentBalanceInUSDCents! - callCost * 100,
);
await ProjectService.updateOneById({
data: {
smsOrCallCurrentBalanceInUSDCents:
project.smsOrCallCurrentBalanceInUSDCents,
notEnabledSmsOrCallNotificationSentToOwners: false, // reset this flag
},
id: project.id!,
props: {
isRoot: true,
},
});
logger.debug("Project's current balance updated.");
logger.debug(
"Current Balance: " + project.smsOrCallCurrentBalanceInUSDCents,
);
}
} catch (e: any) {
callLog.callCostInUSDCents = 0;
callLog.status = CallStatus.Error;
callLog.statusMessage =
e && e.message ? e.message.toString() : e.toString();
logger.error("Call Request failed.");
logger.error(callLog.statusMessage);
callError = e;
} }
public static generateTwimlForCall(callRequest: CallRequest): string { logger.debug("Saving Call Log if project id is provided.");
const response: Twilio.twiml.VoiceResponse =
new Twilio.twiml.VoiceResponse();
for (const item of callRequest.data) { if (options.projectId) {
if ((item as Say).sayMessage) { logger.debug("Saving Call Log.");
response.say((item as Say).sayMessage); await CallLogService.create({
} data: callLog,
props: {
if ((item as GatherInput) && (item as GatherInput).numDigits > 0) { isRoot: true,
response.say((item as GatherInput).introMessage); },
});
response.gather({ logger.debug("Call Log saved.");
numDigits: (item as GatherInput).numDigits, } else {
timeout: (item as GatherInput).timeoutInSeconds || 5, logger.debug("Project Id is not provided. Call Log not saved.");
action: (item as GatherInput).responseUrl
.addQueryParam(
'token',
JSONWebToken.signJsonPayload(
JSONFunctions.serialize(
(item as GatherInput)
.onInputCallRequest as any
),
OneUptimeDate.getDayInSeconds()
)
)
.toString(),
method: 'POST',
});
response.say((item as GatherInput).noInputMessage);
}
}
response.hangup();
return response.toString();
} }
if (options.userOnCallLogTimelineId) {
await UserOnCallLogTimelineService.updateOneById({
data: {
status:
callLog.status === CallStatus.Success
? UserNotificationStatus.Sent
: UserNotificationStatus.Error,
statusMessage: callLog.statusMessage!,
},
id: options.userOnCallLogTimelineId,
props: {
isRoot: true,
},
});
}
if (callError) {
throw callError;
}
}
public static generateTwimlForCall(callRequest: CallRequest): string {
const response: Twilio.twiml.VoiceResponse =
new Twilio.twiml.VoiceResponse();
for (const item of callRequest.data) {
if ((item as Say).sayMessage) {
response.say((item as Say).sayMessage);
}
if ((item as GatherInput) && (item as GatherInput).numDigits > 0) {
response.say((item as GatherInput).introMessage);
response.gather({
numDigits: (item as GatherInput).numDigits,
timeout: (item as GatherInput).timeoutInSeconds || 5,
action: (item as GatherInput).responseUrl
.addQueryParam(
"token",
JSONWebToken.signJsonPayload(
JSONFunctions.serialize(
(item as GatherInput).onInputCallRequest as any,
),
OneUptimeDate.getDayInSeconds(),
),
)
.toString(),
method: "POST",
});
response.say((item as GatherInput).noInputMessage);
}
}
response.hangup();
return response.toString();
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,319 +1,302 @@
import { import {
SMSDefaultCostInCents, SMSDefaultCostInCents,
SMSHighRiskCostInCents, SMSHighRiskCostInCents,
getTwilioConfig, getTwilioConfig,
} from '../Config'; } from "../Config";
import { isHighRiskPhoneNumber } from 'Common/Types/Call/CallRequest'; import { isHighRiskPhoneNumber } from "Common/Types/Call/CallRequest";
import TwilioConfig from 'Common/Types/CallAndSMS/TwilioConfig'; import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
import BadDataException from 'Common/Types/Exception/BadDataException'; import BadDataException from "Common/Types/Exception/BadDataException";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import Phone from 'Common/Types/Phone'; import Phone from "Common/Types/Phone";
import SmsStatus from 'Common/Types/SmsStatus'; import SmsStatus from "Common/Types/SmsStatus";
import Text from 'Common/Types/Text'; import Text from "Common/Types/Text";
import UserNotificationStatus from 'Common/Types/UserNotification/UserNotificationStatus'; import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus";
import { IsBillingEnabled } from 'CommonServer/EnvironmentConfig'; import { IsBillingEnabled } from "CommonServer/EnvironmentConfig";
import NotificationService from 'CommonServer/Services/NotificationService'; import NotificationService from "CommonServer/Services/NotificationService";
import ProjectService from 'CommonServer/Services/ProjectService'; import ProjectService from "CommonServer/Services/ProjectService";
import SmsLogService from 'CommonServer/Services/SmsLogService'; import SmsLogService from "CommonServer/Services/SmsLogService";
import UserOnCallLogTimelineService from 'CommonServer/Services/UserOnCallLogTimelineService'; import UserOnCallLogTimelineService from "CommonServer/Services/UserOnCallLogTimelineService";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
import SmsLog from 'Model/Models/SmsLog'; import SmsLog from "Model/Models/SmsLog";
import Twilio from 'twilio'; import Twilio from "twilio";
import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
export default class SmsService { export default class SmsService {
public static async sendSms( public static async sendSms(
to: Phone, to: Phone,
message: string, message: string,
options: { options: {
projectId?: ObjectID | undefined; // project id for sms log projectId?: ObjectID | undefined; // project id for sms log
customTwilioConfig?: TwilioConfig | undefined; customTwilioConfig?: TwilioConfig | undefined;
isSensitive?: boolean; // if true, message will not be logged isSensitive?: boolean; // if true, message will not be logged
userOnCallLogTimelineId?: ObjectID | undefined; userOnCallLogTimelineId?: ObjectID | undefined;
},
): Promise<void> {
let smsError: Error | null = null;
const smsLog: SmsLog = new SmsLog();
try {
// check number of sms to send for this entire messages to send. Each sms can have 160 characters.
const smsSegments: number = Math.ceil(message.length / 160);
message = Text.trimLines(message);
let smsCost: number = 0;
const shouldChargeForSMS: boolean =
IsBillingEnabled && !options.customTwilioConfig;
if (shouldChargeForSMS) {
smsCost = SMSDefaultCostInCents / 100;
if (isHighRiskPhoneNumber(to)) {
smsCost = SMSHighRiskCostInCents / 100;
} }
): Promise<void> { }
let smsError: Error | null = null;
const smsLog: SmsLog = new SmsLog();
try { if (smsSegments > 1) {
// check number of sms to send for this entire messages to send. Each sms can have 160 characters. smsCost = smsCost * smsSegments;
const smsSegments: number = Math.ceil(message.length / 160); }
message = Text.trimLines(message); smsLog.toNumber = to;
let smsCost: number = 0; smsLog.smsText =
options && options.isSensitive
? "This message is sensitive and is not logged"
: message;
smsLog.smsCostInUSDCents = 0;
const shouldChargeForSMS: boolean = if (options.projectId) {
IsBillingEnabled && !options.customTwilioConfig; smsLog.projectId = options.projectId;
}
if (shouldChargeForSMS) { const twilioConfig: TwilioConfig | null =
smsCost = SMSDefaultCostInCents / 100; options.customTwilioConfig || (await getTwilioConfig());
if (isHighRiskPhoneNumber(to)) { if (!twilioConfig) {
smsCost = SMSHighRiskCostInCents / 100; throw new BadDataException("Twilio Config not found");
} }
}
if (smsSegments > 1) { const client: Twilio.Twilio = Twilio(
smsCost = smsCost * smsSegments; twilioConfig.accountSid,
} twilioConfig.authToken,
);
smsLog.toNumber = to; smsLog.fromNumber = twilioConfig.phoneNumber;
smsLog.smsText = let project: Project | null = null;
options && options.isSensitive
? 'This message is sensitive and is not logged'
: message;
smsLog.smsCostInUSDCents = 0;
if (options.projectId) { // make sure project has enough balance.
smsLog.projectId = options.projectId;
}
const twilioConfig: TwilioConfig | null = if (options.projectId) {
options.customTwilioConfig || (await getTwilioConfig()); project = await ProjectService.findOneById({
id: options.projectId,
select: {
smsOrCallCurrentBalanceInUSDCents: true,
enableSmsNotifications: true,
lowCallAndSMSBalanceNotificationSentToOwners: true,
name: true,
notEnabledSmsOrCallNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
if (!twilioConfig) { if (!project) {
throw new BadDataException('Twilio Config not found'); smsLog.status = SmsStatus.Error;
} smsLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
logger.error(smsLog.statusMessage);
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true,
},
});
return;
}
const client: Twilio.Twilio = Twilio( if (!project.enableSmsNotifications) {
twilioConfig.accountSid, smsLog.status = SmsStatus.Error;
twilioConfig.authToken smsLog.statusMessage = `SMS notifications are not enabled for this project. Please enable SMS notifications in Project Settings.`;
logger.error(smsLog.statusMessage);
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true,
},
});
if (!project.notEnabledSmsOrCallNotificationSentToOwners) {
await ProjectService.updateOneById({
data: {
notEnabledSmsOrCallNotificationSentToOwners: true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
"SMS notifications not enabled for " + (project.name || ""),
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/> <br/> This SMS was not sent because SMS notifications are not enabled for this project. Please enable SMS notifications in Project Settings.`,
); );
}
return;
}
smsLog.fromNumber = twilioConfig.phoneNumber; if (shouldChargeForSMS) {
// check if auto recharge is enabled and current balance is low.
let updatedBalance: number =
project.smsOrCallCurrentBalanceInUSDCents!;
try {
updatedBalance = await NotificationService.rechargeIfBalanceIsLow(
project.id!,
);
} catch (err) {
logger.error(err);
}
let project: Project | null = null; project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
// make sure project has enough balance. if (!project.smsOrCallCurrentBalanceInUSDCents) {
smsLog.status = SmsStatus.LowBalance;
if (options.projectId) { smsLog.statusMessage = `Project ${options.projectId.toString()} does not have enough SMS balance.`;
project = await ProjectService.findOneById({
id: options.projectId,
select: {
smsOrCallCurrentBalanceInUSDCents: true,
enableSmsNotifications: true,
lowCallAndSMSBalanceNotificationSentToOwners: true,
name: true,
notEnabledSmsOrCallNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
if (!project) {
smsLog.status = SmsStatus.Error;
smsLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
logger.error(smsLog.statusMessage);
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true,
},
});
return;
}
if (!project.enableSmsNotifications) {
smsLog.status = SmsStatus.Error;
smsLog.statusMessage = `SMS notifications are not enabled for this project. Please enable SMS notifications in Project Settings.`;
logger.error(smsLog.statusMessage);
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true,
},
});
if (!project.notEnabledSmsOrCallNotificationSentToOwners) {
await ProjectService.updateOneById({
data: {
notEnabledSmsOrCallNotificationSentToOwners:
true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
'SMS notifications not enabled for ' +
(project.name || ''),
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/> <br/> This SMS was not sent because SMS notifications are not enabled for this project. Please enable SMS notifications in Project Settings.`
);
}
return;
}
if (shouldChargeForSMS) {
// check if auto recharge is enabled and current balance is low.
let updatedBalance: number =
project.smsOrCallCurrentBalanceInUSDCents!;
try {
updatedBalance =
await NotificationService.rechargeIfBalanceIsLow(
project.id!
);
} catch (err) {
logger.error(err);
}
project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
if (!project.smsOrCallCurrentBalanceInUSDCents) {
smsLog.status = SmsStatus.LowBalance;
smsLog.statusMessage = `Project ${options.projectId.toString()} does not have enough SMS balance.`;
logger.error(smsLog.statusMessage);
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true,
},
});
if (
!project.lowCallAndSMSBalanceNotificationSentToOwners
) {
await ProjectService.updateOneById({
data: {
lowCallAndSMSBalanceNotificationSentToOwners:
true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
'Low SMS and Call Balance for ' +
(project.name || ''),
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/>This SMS was not sent because project does not have enough balance to send SMS. Current balance is ${
(project.smsOrCallCurrentBalanceInUSDCents ||
0) / 100
} USD cents. Required balance to send this SMS should is ${smsCost} USD. Please enable auto recharge or recharge manually.`
);
}
return;
}
if (
project.smsOrCallCurrentBalanceInUSDCents <
smsCost * 100
) {
smsLog.status = SmsStatus.LowBalance;
smsLog.statusMessage = `Project does not have enough balance to send SMS. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${smsCost} USD to send this SMS.`;
logger.error(smsLog.statusMessage);
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true,
},
});
if (
!project.lowCallAndSMSBalanceNotificationSentToOwners
) {
await ProjectService.updateOneById({
data: {
lowCallAndSMSBalanceNotificationSentToOwners:
true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
'Low SMS and Call Balance for ' +
(project.name || ''),
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/> <br/> This SMS was not sent because project does not have enough balance to send SMS. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents /
100
} USD. Required balance is ${smsCost} USD to send this SMS. Please enable auto recharge or recharge manually.`
);
}
return;
}
}
}
const twillioMessage: MessageInstance =
await client.messages.create({
body: message,
to: to.toString(),
from: twilioConfig.phoneNumber.toString(), // From a valid Twilio number
});
smsLog.status = SmsStatus.Success;
smsLog.statusMessage = 'Message ID: ' + twillioMessage.sid;
logger.debug('SMS message sent successfully.');
logger.debug(smsLog.statusMessage);
if (shouldChargeForSMS && project) {
smsLog.smsCostInUSDCents = smsCost * 100;
project.smsOrCallCurrentBalanceInUSDCents = Math.floor(
project.smsOrCallCurrentBalanceInUSDCents! - smsCost * 100
);
await ProjectService.updateOneById({
data: {
smsOrCallCurrentBalanceInUSDCents:
project.smsOrCallCurrentBalanceInUSDCents,
notEnabledSmsOrCallNotificationSentToOwners: false, // reset this flag
},
id: project.id!,
props: {
isRoot: true,
},
});
}
} catch (e: any) {
smsLog.smsCostInUSDCents = 0;
smsLog.status = SmsStatus.Error;
smsLog.statusMessage =
e && e.message ? e.message.toString() : e.toString();
logger.error('SMS message failed to send.');
logger.error(smsLog.statusMessage); logger.error(smsLog.statusMessage);
smsError = e;
}
if (options.projectId) {
await SmsLogService.create({ await SmsLogService.create({
data: smsLog, data: smsLog,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
}
if (options.userOnCallLogTimelineId) { if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
await UserOnCallLogTimelineService.updateOneById({ await ProjectService.updateOneById({
data: { data: {
status: lowCallAndSMSBalanceNotificationSentToOwners: true,
smsLog.status === SmsStatus.Success
? UserNotificationStatus.Sent
: UserNotificationStatus.Error,
statusMessage: smsLog.statusMessage!,
}, },
id: options.userOnCallLogTimelineId, id: project.id!,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
} await ProjectService.sendEmailToProjectOwners(
project.id!,
"Low SMS and Call Balance for " + (project.name || ""),
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/>This SMS was not sent because project does not have enough balance to send SMS. Current balance is ${
(project.smsOrCallCurrentBalanceInUSDCents || 0) / 100
} USD cents. Required balance to send this SMS should is ${smsCost} USD. Please enable auto recharge or recharge manually.`,
);
}
return;
}
if (smsError) { if (project.smsOrCallCurrentBalanceInUSDCents < smsCost * 100) {
throw smsError; smsLog.status = SmsStatus.LowBalance;
smsLog.statusMessage = `Project does not have enough balance to send SMS. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${smsCost} USD to send this SMS.`;
logger.error(smsLog.statusMessage);
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true,
},
});
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
await ProjectService.updateOneById({
data: {
lowCallAndSMSBalanceNotificationSentToOwners: true,
},
id: project.id!,
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
"Low SMS and Call Balance for " + (project.name || ""),
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/> <br/> This SMS was not sent because project does not have enough balance to send SMS. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${smsCost} USD to send this SMS. Please enable auto recharge or recharge manually.`,
);
}
return;
}
} }
}
const twillioMessage: MessageInstance = await client.messages.create({
body: message,
to: to.toString(),
from: twilioConfig.phoneNumber.toString(), // From a valid Twilio number
});
smsLog.status = SmsStatus.Success;
smsLog.statusMessage = "Message ID: " + twillioMessage.sid;
logger.debug("SMS message sent successfully.");
logger.debug(smsLog.statusMessage);
if (shouldChargeForSMS && project) {
smsLog.smsCostInUSDCents = smsCost * 100;
project.smsOrCallCurrentBalanceInUSDCents = Math.floor(
project.smsOrCallCurrentBalanceInUSDCents! - smsCost * 100,
);
await ProjectService.updateOneById({
data: {
smsOrCallCurrentBalanceInUSDCents:
project.smsOrCallCurrentBalanceInUSDCents,
notEnabledSmsOrCallNotificationSentToOwners: false, // reset this flag
},
id: project.id!,
props: {
isRoot: true,
},
});
}
} catch (e: any) {
smsLog.smsCostInUSDCents = 0;
smsLog.status = SmsStatus.Error;
smsLog.statusMessage =
e && e.message ? e.message.toString() : e.toString();
logger.error("SMS message failed to send.");
logger.error(smsLog.statusMessage);
smsError = e;
} }
if (options.projectId) {
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true,
},
});
}
if (options.userOnCallLogTimelineId) {
await UserOnCallLogTimelineService.updateOneById({
data: {
status:
smsLog.status === SmsStatus.Success
? UserNotificationStatus.Sent
: UserNotificationStatus.Error,
statusMessage: smsLog.statusMessage!,
},
id: options.userOnCallLogTimelineId,
props: {
isRoot: true,
},
});
}
if (smsError) {
throw smsError;
}
}
} }

View File

@@ -1,64 +1,64 @@
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes'; import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import logger from 'CommonServer/Utils/Logger'; import logger from "CommonServer/Utils/Logger";
import fsp from 'fs/promises'; import fsp from "fs/promises";
import Handlebars from 'handlebars'; import Handlebars from "handlebars";
import Path from 'path'; import Path from "path";
const loadPartials: PromiseVoidFunction = async (): Promise<void> => { const loadPartials: PromiseVoidFunction = async (): Promise<void> => {
// get all partials in the partial folder and comile then and register then as partials in handlebars. // get all partials in the partial folder and comile then and register then as partials in handlebars.
const partialsDir: string = Path.resolve( const partialsDir: string = Path.resolve(
process.cwd(), process.cwd(),
'FeatureSet', "FeatureSet",
'Notification', "Notification",
'Templates', "Templates",
'Partials' "Partials",
);
const filenames: string[] = await fsp.readdir(partialsDir);
filenames.forEach(async (filename: string) => {
const matches: RegExpMatchArray | null = filename.match(/^(.*)\.hbs$/);
if (!matches) {
return;
}
const name: string = matches[1]!;
const template: string = await fsp.readFile(
Path.resolve(partialsDir, filename),
{ encoding: "utf8", flag: "r" },
); );
const filenames: string[] = await fsp.readdir(partialsDir);
filenames.forEach(async (filename: string) => {
const matches: RegExpMatchArray | null = filename.match(/^(.*)\.hbs$/);
if (!matches) {
return;
}
const name: string = matches[1]!; const partialTemplate: Handlebars.TemplateDelegate =
const template: string = await fsp.readFile( Handlebars.compile(template);
Path.resolve(partialsDir, filename),
{ encoding: 'utf8', flag: 'r' }
);
const partialTemplate: Handlebars.TemplateDelegate = Handlebars.registerPartial(name, partialTemplate);
Handlebars.compile(template);
Handlebars.registerPartial(name, partialTemplate); logger.debug(`Loaded partial ${name}`);
});
logger.debug(`Loaded partial ${name}`);
});
}; };
loadPartials().catch((err: Error) => { loadPartials().catch((err: Error) => {
logger.error('Error loading partials'); logger.error("Error loading partials");
logger.error(err); logger.error(err);
}); });
Handlebars.registerHelper('ifCond', function (v1, v2, options) { Handlebars.registerHelper("ifCond", function (v1, v2, options) {
if (v1 === v2) { if (v1 === v2) {
//@ts-ignore
return options.fn(this);
}
//@ts-ignore //@ts-ignore
return options.inverse(this); return options.fn(this);
}
//@ts-ignore
return options.inverse(this);
}); });
Handlebars.registerHelper('concat', (v1: any, v2: any) => { Handlebars.registerHelper("concat", (v1: any, v2: any) => {
// contact v1 and v2 // contact v1 and v2
return v1 + v2; return v1 + v2;
}); });
Handlebars.registerHelper('ifNotCond', function (v1, v2, options) { Handlebars.registerHelper("ifNotCond", function (v1, v2, options) {
if (v1 !== v2) { if (v1 !== v2) {
//@ts-ignore
return options.fn(this);
}
//@ts-ignore //@ts-ignore
return options.inverse(this); return options.fn(this);
}
//@ts-ignore
return options.inverse(this);
}); });

View File

@@ -1,34 +1,34 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import AnalyticsTableColumn from 'Common/Types/AnalyticsDatabase/TableColumn'; import AnalyticsTableColumn from "Common/Types/AnalyticsDatabase/TableColumn";
import TableColumnType from 'Common/Types/AnalyticsDatabase/TableColumnType'; import TableColumnType from "Common/Types/AnalyticsDatabase/TableColumnType";
import MetricService from 'CommonServer/Services/MetricService'; import MetricService from "CommonServer/Services/MetricService";
import Metric from 'Model/AnalyticsModels/Metric'; import Metric from "Model/AnalyticsModels/Metric";
export default class AddAggregationTemporalityToMetric extends DataMigrationBase { export default class AddAggregationTemporalityToMetric extends DataMigrationBase {
public constructor() { public constructor() {
super('AddAggregationTemporalityToMetric'); super("AddAggregationTemporalityToMetric");
}
public override async migrate(): Promise<void> {
const column: AnalyticsTableColumn | undefined =
new Metric().tableColumns.find((column: AnalyticsTableColumn) => {
return column.key === "aggregationTemporality";
});
if (!column) {
return;
} }
public override async migrate(): Promise<void> { const columnType: TableColumnType | null =
const column: AnalyticsTableColumn | undefined = await MetricService.getColumnTypeInDatabase(column);
new Metric().tableColumns.find((column: AnalyticsTableColumn) => {
return column.key === 'aggregationTemporality';
});
if (!column) { if (!columnType) {
return; await MetricService.dropColumnInDatabase("aggregationTemporality");
} await MetricService.addColumnInDatabase(column);
const columnType: TableColumnType | null =
await MetricService.getColumnTypeInDatabase(column);
if (!columnType) {
await MetricService.dropColumnInDatabase('aggregationTemporality');
await MetricService.addColumnInDatabase(column);
}
} }
}
public override async rollback(): Promise<void> { public override async rollback(): Promise<void> {
return; return;
} }
} }

View File

@@ -1,63 +1,63 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import AnalyticsTableColumn from 'Common/Types/AnalyticsDatabase/TableColumn'; import AnalyticsTableColumn from "Common/Types/AnalyticsDatabase/TableColumn";
import TableColumnType from 'Common/Types/AnalyticsDatabase/TableColumnType'; import TableColumnType from "Common/Types/AnalyticsDatabase/TableColumnType";
import LogService from 'CommonServer/Services/LogService'; import LogService from "CommonServer/Services/LogService";
import SpanService from 'CommonServer/Services/SpanService'; import SpanService from "CommonServer/Services/SpanService";
import Log from 'Model/AnalyticsModels/Log'; import Log from "Model/AnalyticsModels/Log";
import Span from 'Model/AnalyticsModels/Span'; import Span from "Model/AnalyticsModels/Span";
export default class AddAttributeColumnToSpanAndLog extends DataMigrationBase { export default class AddAttributeColumnToSpanAndLog extends DataMigrationBase {
public constructor() { public constructor() {
super('AddAttributeColumnToSpanAndLog'); super("AddAttributeColumnToSpanAndLog");
}
public override async migrate(): Promise<void> {
await this.addAttributesColumnToLog();
await this.addAttributesColumnToSpan();
}
public async addAttributesColumnToLog(): Promise<void> {
// logs
const logsAttributesColumn: AnalyticsTableColumn | undefined =
new Log().tableColumns.find((column: AnalyticsTableColumn) => {
return column.key === "attributes";
});
if (!logsAttributesColumn) {
return;
} }
public override async migrate(): Promise<void> { const columnType: TableColumnType | null =
await this.addAttributesColumnToLog(); await LogService.getColumnTypeInDatabase(logsAttributesColumn);
await this.addAttributesColumnToSpan();
if (!columnType) {
await LogService.dropColumnInDatabase("attributes");
await LogService.addColumnInDatabase(logsAttributesColumn);
}
}
public async addAttributesColumnToSpan(): Promise<void> {
// spans
const spansAttributesColumn: AnalyticsTableColumn | undefined =
new Span().tableColumns.find((column: AnalyticsTableColumn) => {
return column.key === "attributes";
});
if (!spansAttributesColumn) {
return;
} }
public async addAttributesColumnToLog(): Promise<void> { const spansColumnType: TableColumnType | null =
// logs await SpanService.getColumnTypeInDatabase(spansAttributesColumn);
const logsAttributesColumn: AnalyticsTableColumn | undefined =
new Log().tableColumns.find((column: AnalyticsTableColumn) => {
return column.key === 'attributes';
});
if (!logsAttributesColumn) { if (!spansColumnType) {
return; await SpanService.dropColumnInDatabase("attributes");
} await SpanService.addColumnInDatabase(spansAttributesColumn);
const columnType: TableColumnType | null =
await LogService.getColumnTypeInDatabase(logsAttributesColumn);
if (!columnType) {
await LogService.dropColumnInDatabase('attributes');
await LogService.addColumnInDatabase(logsAttributesColumn);
}
} }
}
public async addAttributesColumnToSpan(): Promise<void> { public override async rollback(): Promise<void> {
// spans return;
}
const spansAttributesColumn: AnalyticsTableColumn | undefined =
new Span().tableColumns.find((column: AnalyticsTableColumn) => {
return column.key === 'attributes';
});
if (!spansAttributesColumn) {
return;
}
const spansColumnType: TableColumnType | null =
await SpanService.getColumnTypeInDatabase(spansAttributesColumn);
if (!spansColumnType) {
await SpanService.dropColumnInDatabase('attributes');
await SpanService.addColumnInDatabase(spansAttributesColumn);
}
}
public override async rollback(): Promise<void> {
return;
}
} }

View File

@@ -1,31 +1,31 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import ObjectID from 'Common/Types/ObjectID'; import ObjectID from "Common/Types/ObjectID";
import GlobalConfigService from 'CommonServer/Services/GlobalConfigService'; import GlobalConfigService from "CommonServer/Services/GlobalConfigService";
import GlobalConfig, { EmailServerType } from 'Model/Models/GlobalConfig'; import GlobalConfig, { EmailServerType } from "Model/Models/GlobalConfig";
export default class AddDefaultGlobalConfig extends DataMigrationBase { export default class AddDefaultGlobalConfig extends DataMigrationBase {
public constructor() { public constructor() {
super('AddDefaultGlobalConfig'); super("AddDefaultGlobalConfig");
} }
public override async migrate(): Promise<void> { public override async migrate(): Promise<void> {
// get all the users with email isVerified true. // get all the users with email isVerified true.
const globalConfig: GlobalConfig = new GlobalConfig(); const globalConfig: GlobalConfig = new GlobalConfig();
globalConfig.id = ObjectID.getZeroObjectID(); globalConfig.id = ObjectID.getZeroObjectID();
globalConfig.emailServerType = EmailServerType.Internal; globalConfig.emailServerType = EmailServerType.Internal;
globalConfig.sendgridFromName = 'OneUptime'; globalConfig.sendgridFromName = "OneUptime";
globalConfig.smtpFromName = 'OneUptime'; globalConfig.smtpFromName = "OneUptime";
await GlobalConfigService.create({ await GlobalConfigService.create({
data: globalConfig, data: globalConfig,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); });
} }
public override async rollback(): Promise<void> { public override async rollback(): Promise<void> {
return; return;
} }
} }

View File

@@ -1,76 +1,76 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import { Green } from 'Common/Types/BrandColors'; import { Green } from "Common/Types/BrandColors";
import Color from 'Common/Types/Color'; import Color from "Common/Types/Color";
import LIMIT_MAX, { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax'; import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import MonitorStatusService from 'CommonServer/Services/MonitorStatusService'; import MonitorStatusService from "CommonServer/Services/MonitorStatusService";
import StatusPageService from 'CommonServer/Services/StatusPageService'; import StatusPageService from "CommonServer/Services/StatusPageService";
import MonitorStatus from 'Model/Models/MonitorStatus'; import MonitorStatus from "Model/Models/MonitorStatus";
import StatusPage from 'Model/Models/StatusPage'; import StatusPage from "Model/Models/StatusPage";
export default class AddDowntimeMonitorStatusToStatusPage extends DataMigrationBase { export default class AddDowntimeMonitorStatusToStatusPage extends DataMigrationBase {
public constructor() { public constructor() {
super('AddDowntimeMonitorStatusToStatusPage'); super("AddDowntimeMonitorStatusToStatusPage");
} }
public override async migrate(): Promise<void> { public override async migrate(): Promise<void> {
// get all the users with email isVerified true. // get all the users with email isVerified true.
const statusPages: Array<StatusPage> = await StatusPageService.findBy({ const statusPages: Array<StatusPage> = await StatusPageService.findBy({
query: {}, query: {},
select: { select: {
_id: true, _id: true,
projectId: true, projectId: true,
}, },
skip: 0, skip: 0,
limit: LIMIT_MAX, limit: LIMIT_MAX,
props: { props: {
isRoot: true, isRoot: true,
}, },
});
for (const statusPage of statusPages) {
// add ended scheduled maintenance state for each of these projects.
// first fetch resolved state. Ended state order is -1 of resolved state.
if (!statusPage.projectId) {
continue;
}
const monitorStatuses: Array<MonitorStatus> =
await MonitorStatusService.findBy({
query: {
projectId: statusPage.projectId,
},
select: {
_id: true,
isOperationalState: true,
},
props: {
isRoot: true,
},
skip: 0,
limit: LIMIT_PER_PROJECT,
}); });
for (const statusPage of statusPages) { const getNonOperationStatuses: Array<MonitorStatus> =
// add ended scheduled maintenance state for each of these projects. monitorStatuses.filter((monitorStatus: MonitorStatus) => {
// first fetch resolved state. Ended state order is -1 of resolved state. return !monitorStatus.isOperationalState;
});
if (!statusPage.projectId) { await StatusPageService.updateOneById({
continue; id: statusPage.id!,
} data: {
downtimeMonitorStatuses: getNonOperationStatuses as any,
const monitorStatuses: Array<MonitorStatus> = defaultBarColor: new Color(Green.toString()) as any,
await MonitorStatusService.findBy({ },
query: { props: {
projectId: statusPage.projectId, isRoot: true,
}, },
select: { });
_id: true,
isOperationalState: true,
},
props: {
isRoot: true,
},
skip: 0,
limit: LIMIT_PER_PROJECT,
});
const getNonOperationStatuses: Array<MonitorStatus> =
monitorStatuses.filter((monitorStatus: MonitorStatus) => {
return !monitorStatus.isOperationalState;
});
await StatusPageService.updateOneById({
id: statusPage.id!,
data: {
downtimeMonitorStatuses: getNonOperationStatuses as any,
defaultBarColor: new Color(Green.toString()) as any,
},
props: {
isRoot: true,
},
});
}
} }
}
public override async rollback(): Promise<void> { public override async rollback(): Promise<void> {
return; return;
} }
} }

View File

@@ -1,33 +1,34 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import AnalyticsTableColumn from 'Common/Types/AnalyticsDatabase/TableColumn'; import AnalyticsTableColumn from "Common/Types/AnalyticsDatabase/TableColumn";
import SpanService from 'CommonServer/Services/SpanService'; import SpanService from "CommonServer/Services/SpanService";
import Span from 'Model/AnalyticsModels/Span'; import Span from "Model/AnalyticsModels/Span";
export default class AddDurationColumnToSpanTable extends DataMigrationBase { export default class AddDurationColumnToSpanTable extends DataMigrationBase {
public constructor() { public constructor() {
super('AddDurationColumnToSpanTable'); super("AddDurationColumnToSpanTable");
}
public override async migrate(): Promise<void> {
const hasDurationColumn: boolean =
await SpanService.doesColumnExistInDatabase("durationUnixNano");
const durationColumn: AnalyticsTableColumn = new Span().tableColumns.find(
(column: AnalyticsTableColumn) => {
return column.key === "durationUnixNano";
},
)!;
if (!hasDurationColumn && durationColumn) {
await SpanService.addColumnInDatabase(durationColumn);
} }
}
public override async migrate(): Promise<void> { public override async rollback(): Promise<void> {
const hasDurationColumn: boolean = const hasDurationColumn: boolean =
await SpanService.doesColumnExistInDatabase('durationUnixNano'); await SpanService.doesColumnExistInDatabase("durationUnixNano");
const durationColumn: AnalyticsTableColumn = if (hasDurationColumn) {
new Span().tableColumns.find((column: AnalyticsTableColumn) => { await SpanService.dropColumnInDatabase("durationUnixNano");
return column.key === 'durationUnixNano';
})!;
if (!hasDurationColumn && durationColumn) {
await SpanService.addColumnInDatabase(durationColumn);
}
}
public override async rollback(): Promise<void> {
const hasDurationColumn: boolean =
await SpanService.doesColumnExistInDatabase('durationUnixNano');
if (hasDurationColumn) {
await SpanService.dropColumnInDatabase('durationUnixNano');
}
} }
}
} }

View File

@@ -1,111 +1,107 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import SortOrder from 'Common/Types/BaseDatabase/SortOrder'; import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import LIMIT_MAX from 'Common/Types/Database/LimitMax'; import LIMIT_MAX from "Common/Types/Database/LimitMax";
import IncidentService from 'CommonServer/Services/IncidentService'; import IncidentService from "CommonServer/Services/IncidentService";
import IncidentStateTimelineService from 'CommonServer/Services/IncidentStateTimelineService'; import IncidentStateTimelineService from "CommonServer/Services/IncidentStateTimelineService";
import ProjectService from 'CommonServer/Services/ProjectService'; import ProjectService from "CommonServer/Services/ProjectService";
import QueryHelper from 'CommonServer/Types/Database/QueryHelper'; import QueryHelper from "CommonServer/Types/Database/QueryHelper";
import Incident from 'Model/Models/Incident'; import Incident from "Model/Models/Incident";
import IncidentStateTimeline from 'Model/Models/IncidentStateTimeline'; import IncidentStateTimeline from "Model/Models/IncidentStateTimeline";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
export default class AddEndDateToIncidentStateTimeline extends DataMigrationBase { export default class AddEndDateToIncidentStateTimeline extends DataMigrationBase {
public constructor() { public constructor() {
super('AddEndDateToIncidentStateTimeline'); super("AddEndDateToIncidentStateTimeline");
} }
public override async migrate(): Promise<void> { public override async migrate(): Promise<void> {
// get all the users with email isVerified true. // get all the users with email isVerified true.
const projects: Array<Project> = await ProjectService.findBy({ const projects: Array<Project> = await ProjectService.findBy({
query: {}, query: {},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const project of projects) {
// add ended scheduled maintenance state for each of these projects.
// first fetch resolved state. Ended state order is -1 of resolved state.
const incidents: Array<Incident> = await IncidentService.findBy({
query: {
projectId: project.id!,
},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const incident of incidents) {
const incidentStateTimelines: Array<IncidentStateTimeline> =
await IncidentStateTimelineService.findBy({
query: {
incidentId: incident.id!,
endsAt: QueryHelper.isNull(),
},
select: { select: {
_id: true, _id: true,
createdAt: true,
}, },
skip: 0, skip: 0,
limit: LIMIT_MAX, limit: LIMIT_MAX,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); sort: {
createdAt: SortOrder.Ascending,
},
});
for (const project of projects) { for (let i: number = 0; i < incidentStateTimelines.length; i++) {
// add ended scheduled maintenance state for each of these projects. const statusTimeline: IncidentStateTimeline | undefined =
// first fetch resolved state. Ended state order is -1 of resolved state. incidentStateTimelines[i];
const incidents: Array<Incident> = await IncidentService.findBy({ if (!statusTimeline) {
query: { continue;
projectId: project.id!, }
},
select: { let endDate: Date | null = null;
_id: true,
}, if (
skip: 0, incidentStateTimelines[i + 1] &&
limit: LIMIT_MAX, incidentStateTimelines[i + 1]?.createdAt
props: { ) {
isRoot: true, endDate = incidentStateTimelines[i + 1]!.createdAt!;
}, }
if (endDate) {
await IncidentStateTimelineService.updateOneById({
id: statusTimeline!.id!,
data: {
endsAt: endDate,
},
props: {
isRoot: true,
},
}); });
}
for (const incident of incidents) {
const incidentStateTimelines: Array<IncidentStateTimeline> =
await IncidentStateTimelineService.findBy({
query: {
incidentId: incident.id!,
endsAt: QueryHelper.isNull(),
},
select: {
_id: true,
createdAt: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
sort: {
createdAt: SortOrder.Ascending,
},
});
for (
let i: number = 0;
i < incidentStateTimelines.length;
i++
) {
const statusTimeline: IncidentStateTimeline | undefined =
incidentStateTimelines[i];
if (!statusTimeline) {
continue;
}
let endDate: Date | null = null;
if (
incidentStateTimelines[i + 1] &&
incidentStateTimelines[i + 1]?.createdAt
) {
endDate = incidentStateTimelines[i + 1]!.createdAt!;
}
if (endDate) {
await IncidentStateTimelineService.updateOneById({
id: statusTimeline!.id!,
data: {
endsAt: endDate,
},
props: {
isRoot: true,
},
});
}
}
}
} }
}
} }
}
public override async rollback(): Promise<void> { public override async rollback(): Promise<void> {
return; return;
} }
} }

View File

@@ -1,107 +1,104 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import SortOrder from 'Common/Types/BaseDatabase/SortOrder'; import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import LIMIT_MAX from 'Common/Types/Database/LimitMax'; import LIMIT_MAX from "Common/Types/Database/LimitMax";
import MonitorService from 'CommonServer/Services/MonitorService'; import MonitorService from "CommonServer/Services/MonitorService";
import MonitorStatusTimelineService from 'CommonServer/Services/MonitorStatusTimelineService'; import MonitorStatusTimelineService from "CommonServer/Services/MonitorStatusTimelineService";
import ProjectService from 'CommonServer/Services/ProjectService'; import ProjectService from "CommonServer/Services/ProjectService";
import QueryHelper from 'CommonServer/Types/Database/QueryHelper'; import QueryHelper from "CommonServer/Types/Database/QueryHelper";
import Monitor from 'Model/Models/Monitor'; import Monitor from "Model/Models/Monitor";
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline'; import MonitorStatusTimeline from "Model/Models/MonitorStatusTimeline";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
export default class AddEndDateToMonitorStatusTimeline extends DataMigrationBase { export default class AddEndDateToMonitorStatusTimeline extends DataMigrationBase {
public constructor() { public constructor() {
super('AddEndDateToMonitorStatusTimeline'); super("AddEndDateToMonitorStatusTimeline");
} }
public override async migrate(): Promise<void> { public override async migrate(): Promise<void> {
// get all the users with email isVerified true. // get all the users with email isVerified true.
const projects: Array<Project> = await ProjectService.findBy({ const projects: Array<Project> = await ProjectService.findBy({
query: {}, query: {},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const project of projects) {
// add ended scheduled maintenance state for each of these projects.
// first fetch resolved state. Ended state order is -1 of resolved state.
const monitors: Array<Monitor> = await MonitorService.findBy({
query: {
projectId: project.id!,
},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const monitor of monitors) {
const statusTimelines: Array<MonitorStatusTimeline> =
await MonitorStatusTimelineService.findBy({
query: {
monitorId: monitor.id!,
endsAt: QueryHelper.isNull(),
},
select: { select: {
_id: true, _id: true,
createdAt: true,
}, },
skip: 0, skip: 0,
limit: LIMIT_MAX, limit: LIMIT_MAX,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); sort: {
createdAt: SortOrder.Ascending,
},
});
for (const project of projects) { for (let i: number = 0; i < statusTimelines.length; i++) {
// add ended scheduled maintenance state for each of these projects. const statusTimeline: MonitorStatusTimeline | undefined =
// first fetch resolved state. Ended state order is -1 of resolved state. statusTimelines[i];
const monitors: Array<Monitor> = await MonitorService.findBy({ if (!statusTimeline) {
query: { continue;
projectId: project.id!, }
},
select: { let endDate: Date | null = null;
_id: true,
}, if (statusTimelines[i + 1] && statusTimelines[i + 1]?.createdAt) {
skip: 0, endDate = statusTimelines[i + 1]!.createdAt!;
limit: LIMIT_MAX, }
props: {
isRoot: true, if (endDate) {
}, await MonitorStatusTimelineService.updateOneById({
id: statusTimeline!.id!,
data: {
endsAt: endDate,
},
props: {
isRoot: true,
},
}); });
}
for (const monitor of monitors) {
const statusTimelines: Array<MonitorStatusTimeline> =
await MonitorStatusTimelineService.findBy({
query: {
monitorId: monitor.id!,
endsAt: QueryHelper.isNull(),
},
select: {
_id: true,
createdAt: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
sort: {
createdAt: SortOrder.Ascending,
},
});
for (let i: number = 0; i < statusTimelines.length; i++) {
const statusTimeline: MonitorStatusTimeline | undefined =
statusTimelines[i];
if (!statusTimeline) {
continue;
}
let endDate: Date | null = null;
if (
statusTimelines[i + 1] &&
statusTimelines[i + 1]?.createdAt
) {
endDate = statusTimelines[i + 1]!.createdAt!;
}
if (endDate) {
await MonitorStatusTimelineService.updateOneById({
id: statusTimeline!.id!,
data: {
endsAt: endDate,
},
props: {
isRoot: true,
},
});
}
}
}
} }
}
} }
}
public override async rollback(): Promise<void> { public override async rollback(): Promise<void> {
return; return;
} }
} }

View File

@@ -1,113 +1,113 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import SortOrder from 'Common/Types/BaseDatabase/SortOrder'; import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import LIMIT_MAX from 'Common/Types/Database/LimitMax'; import LIMIT_MAX from "Common/Types/Database/LimitMax";
import MonitorService from 'CommonServer/Services/MonitorService'; import MonitorService from "CommonServer/Services/MonitorService";
import MonitorStatusTimelineService from 'CommonServer/Services/MonitorStatusTimelineService'; import MonitorStatusTimelineService from "CommonServer/Services/MonitorStatusTimelineService";
import ProjectService from 'CommonServer/Services/ProjectService'; import ProjectService from "CommonServer/Services/ProjectService";
import Monitor from 'Model/Models/Monitor'; import Monitor from "Model/Models/Monitor";
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline'; import MonitorStatusTimeline from "Model/Models/MonitorStatusTimeline";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
export default class AddEndDateToMonitorStatusTimelineWhereEndDateIsMissing extends DataMigrationBase { export default class AddEndDateToMonitorStatusTimelineWhereEndDateIsMissing extends DataMigrationBase {
public constructor() { public constructor() {
super('AddEndDateToMonitorStatusTimelineWhereEndDateIsMissing'); super("AddEndDateToMonitorStatusTimelineWhereEndDateIsMissing");
} }
public override async migrate(): Promise<void> { public override async migrate(): Promise<void> {
// get all the users with email isVerified true. // get all the users with email isVerified true.
const projects: Array<Project> = await ProjectService.findBy({ const projects: Array<Project> = await ProjectService.findBy({
query: {}, query: {},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const project of projects) {
// add ended scheduled maintenance state for each of these projects.
// first fetch resolved state. Ended state order is -1 of resolved state.
const monitors: Array<Monitor> = await MonitorService.findBy({
query: {
projectId: project.id!,
},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const monitor of monitors) {
let statusTimelines: Array<MonitorStatusTimeline> =
await MonitorStatusTimelineService.findBy({
query: {
monitorId: monitor.id!,
},
select: { select: {
_id: true, _id: true,
createdAt: true,
}, },
skip: 0, skip: 0,
limit: LIMIT_MAX, limit: LIMIT_MAX,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); sort: {
createdAt: SortOrder.Descending,
},
});
for (const project of projects) { // reverse the status timelines
// add ended scheduled maintenance state for each of these projects. statusTimelines = statusTimelines.reverse();
// first fetch resolved state. Ended state order is -1 of resolved state.
const monitors: Array<Monitor> = await MonitorService.findBy({ for (let i: number = 0; i < statusTimelines.length; i++) {
query: { const statusTimeline: MonitorStatusTimeline | undefined =
projectId: project.id!, statusTimelines[i];
},
select: { if (!statusTimeline) {
_id: true, continue;
}, }
skip: 0,
limit: LIMIT_MAX, if (statusTimeline.endsAt) {
props: { continue;
isRoot: true, }
},
let endDate: Date | null = statusTimeline.endsAt || null;
if (
!endDate &&
statusTimelines[i + 1] &&
statusTimelines[i + 1]?.createdAt
) {
endDate = statusTimelines[i + 1]!.createdAt!;
}
if (endDate) {
await MonitorStatusTimelineService.updateOneById({
id: statusTimeline!.id!,
data: {
endsAt: endDate,
},
props: {
isRoot: true,
},
}); });
}
for (const monitor of monitors) {
let statusTimelines: Array<MonitorStatusTimeline> =
await MonitorStatusTimelineService.findBy({
query: {
monitorId: monitor.id!,
},
select: {
_id: true,
createdAt: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
sort: {
createdAt: SortOrder.Descending,
},
});
// reverse the status timelines
statusTimelines = statusTimelines.reverse();
for (let i: number = 0; i < statusTimelines.length; i++) {
const statusTimeline: MonitorStatusTimeline | undefined =
statusTimelines[i];
if (!statusTimeline) {
continue;
}
if (statusTimeline.endsAt) {
continue;
}
let endDate: Date | null = statusTimeline.endsAt || null;
if (
!endDate &&
statusTimelines[i + 1] &&
statusTimelines[i + 1]?.createdAt
) {
endDate = statusTimelines[i + 1]!.createdAt!;
}
if (endDate) {
await MonitorStatusTimelineService.updateOneById({
id: statusTimeline!.id!,
data: {
endsAt: endDate,
},
props: {
isRoot: true,
},
});
}
}
}
} }
}
} }
}
public override async rollback(): Promise<void> { public override async rollback(): Promise<void> {
return; return;
} }
} }

View File

@@ -1,117 +1,112 @@
import DataMigrationBase from './DataMigrationBase'; import DataMigrationBase from "./DataMigrationBase";
import SortOrder from 'Common/Types/BaseDatabase/SortOrder'; import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import LIMIT_MAX from 'Common/Types/Database/LimitMax'; import LIMIT_MAX from "Common/Types/Database/LimitMax";
import ProjectService from 'CommonServer/Services/ProjectService'; import ProjectService from "CommonServer/Services/ProjectService";
import ScheduledMaintenanceService from 'CommonServer/Services/ScheduledMaintenanceService'; import ScheduledMaintenanceService from "CommonServer/Services/ScheduledMaintenanceService";
import ScheduledMaintenanceStateTimelineService from 'CommonServer/Services/ScheduledMaintenanceStateTimelineService'; import ScheduledMaintenanceStateTimelineService from "CommonServer/Services/ScheduledMaintenanceStateTimelineService";
import QueryHelper from 'CommonServer/Types/Database/QueryHelper'; import QueryHelper from "CommonServer/Types/Database/QueryHelper";
import Project from 'Model/Models/Project'; import Project from "Model/Models/Project";
import ScheduledMaintenance from 'Model/Models/ScheduledMaintenance'; import ScheduledMaintenance from "Model/Models/ScheduledMaintenance";
import ScheduledMaintenanceStateTimeline from 'Model/Models/ScheduledMaintenanceStateTimeline'; import ScheduledMaintenanceStateTimeline from "Model/Models/ScheduledMaintenanceStateTimeline";
export default class AddEndDateToScheduledEventsStateTimeline extends DataMigrationBase { export default class AddEndDateToScheduledEventsStateTimeline extends DataMigrationBase {
public constructor() { public constructor() {
super('AddEndDateToScheduledEventsStateTimeline'); super("AddEndDateToScheduledEventsStateTimeline");
} }
public override async migrate(): Promise<void> { public override async migrate(): Promise<void> {
// get all the users with email isVerified true. // get all the users with email isVerified true.
const projects: Array<Project> = await ProjectService.findBy({ const projects: Array<Project> = await ProjectService.findBy({
query: {}, query: {},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const project of projects) {
// add ended scheduled maintenance state for each of these projects.
// first fetch resolved state. Ended state order is -1 of resolved state.
const scheduledEvents: Array<ScheduledMaintenance> =
await ScheduledMaintenanceService.findBy({
query: {
projectId: project.id!,
},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const scheduledEvent of scheduledEvents) {
const scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
await ScheduledMaintenanceStateTimelineService.findBy({
query: {
scheduledMaintenanceId: scheduledEvent.id!,
endsAt: QueryHelper.isNull(),
},
select: { select: {
_id: true, _id: true,
createdAt: true,
}, },
skip: 0, skip: 0,
limit: LIMIT_MAX, limit: LIMIT_MAX,
props: { props: {
isRoot: true, isRoot: true,
}, },
}); sort: {
createdAt: SortOrder.Ascending,
},
});
for (const project of projects) { for (
// add ended scheduled maintenance state for each of these projects. let i: number = 0;
// first fetch resolved state. Ended state order is -1 of resolved state. i < scheduledMaintenanceStateTimelines.length;
i++
) {
const statusTimeline: ScheduledMaintenanceStateTimeline | undefined =
scheduledMaintenanceStateTimelines[i];
const scheduledEvents: Array<ScheduledMaintenance> = if (!statusTimeline) {
await ScheduledMaintenanceService.findBy({ continue;
query: { }
projectId: project.id!,
},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const scheduledEvent of scheduledEvents) { let endDate: Date | null = null;
const scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
await ScheduledMaintenanceStateTimelineService.findBy({
query: {
scheduledMaintenanceId: scheduledEvent.id!,
endsAt: QueryHelper.isNull(),
},
select: {
_id: true,
createdAt: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
sort: {
createdAt: SortOrder.Ascending,
},
});
for ( if (
let i: number = 0; scheduledMaintenanceStateTimelines[i + 1] &&
i < scheduledMaintenanceStateTimelines.length; scheduledMaintenanceStateTimelines[i + 1]?.createdAt
i++ ) {
) { endDate = scheduledMaintenanceStateTimelines[i + 1]!.createdAt!;
const statusTimeline: }
| ScheduledMaintenanceStateTimeline
| undefined = scheduledMaintenanceStateTimelines[i];
if (!statusTimeline) { if (endDate) {
continue; await ScheduledMaintenanceStateTimelineService.updateOneById({
} id: statusTimeline!.id!,
data: {
let endDate: Date | null = null; endsAt: endDate,
},
if ( props: {
scheduledMaintenanceStateTimelines[i + 1] && isRoot: true,
scheduledMaintenanceStateTimelines[i + 1]?.createdAt },
) { });
endDate = }
scheduledMaintenanceStateTimelines[i + 1]!
.createdAt!;
}
if (endDate) {
await ScheduledMaintenanceStateTimelineService.updateOneById(
{
id: statusTimeline!.id!,
data: {
endsAt: endDate,
},
props: {
isRoot: true,
},
}
);
}
}
}
} }
}
} }
}
public override async rollback(): Promise<void> { public override async rollback(): Promise<void> {
return; return;
} }
} }

Some files were not shown because too many files have changed in this diff Show More