refactor: Update import statements for TimezoneUtil in multiple files

This commit is contained in:
Simon Larsen
2024-06-17 21:01:44 +01:00
parent 139ff2d638
commit a06a0f1d16
6 changed files with 300 additions and 32 deletions

2
.gitignore vendored
View File

@@ -98,6 +98,8 @@ Llama/Models/llama*
Llama/__pycache__/*
Llama/Models/*
Examples/otel-dotnet/obj/*
InfrastructureAgent/sea-prep.blob

View File

@@ -1,5 +1,6 @@
import ForgotPasswordPage from "./Pages/ForgotPassword";
import LoginPage from "./Pages/Login";
import LoginWithSSO from "./Pages/LoginWithSSO";
import NotFound from "./Pages/NotFound";
import RegisterPage from "./Pages/Register";
import ResetPasswordPage from "./Pages/ResetPassword";
@@ -24,6 +25,8 @@ function App(): ReactElement {
<Routes>
<Route path="/accounts" element={<LoginPage />} />
<Route path="/accounts/login" element={<LoginPage />} />
<Route path="/accounts/sso" element={<LoginWithSSO />} />
<Route
path="/accounts/forgot-password"
element={<ForgotPasswordPage />}

View File

@@ -2,7 +2,6 @@ import { LOGIN_API_URL } from "../Utils/ApiPaths";
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
import { JSONObject } from "Common/Types/JSON";
import Alert, { AlertType } from "CommonUI/src/Components/Alerts/Alert";
import ModelForm, { FormType } from "CommonUI/src/Components/Forms/ModelForm";
import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import Link from "CommonUI/src/Components/Link/Link";
@@ -13,7 +12,7 @@ import LoginUtil from "CommonUI/src/Utils/Login";
import Navigation from "CommonUI/src/Utils/Navigation";
import UserUtil from "CommonUI/src/Utils/User";
import User from "Model/Models/User";
import React, { useState } from "react";
import React from "react";
import useAsyncEffect from "use-async-effect";
const LoginPage: () => JSX.Element = () => {
@@ -23,11 +22,6 @@ const LoginPage: () => JSX.Element = () => {
Navigation.navigate(DASHBOARD_URL);
}
const showSsoMessage: boolean = Boolean(
Navigation.getQueryStringByName("sso"),
);
const [showSsoTip, setShowSSOTip] = useState<boolean>(false);
const [initialValues, setInitialValues] = React.useState<JSONObject>({});
@@ -56,16 +50,6 @@ const LoginPage: () => JSX.Element = () => {
</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>
@@ -120,23 +104,13 @@ const LoginPage: () => JSX.Element = () => {
footer={
<div className="actions text-center mt-4 hover:underline fw-semibold">
<div>
{!showSsoTip && (
<Link to={new Route("/accounts/sso")}>
<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>
)}
</Link>
</div>
</div>
}

View File

@@ -0,0 +1,206 @@
import { SERVICE_PROVIDER_LOGIN_URL } from "../Utils/ApiPaths";
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import Link from "CommonUI/src/Components/Link/Link";
import { DASHBOARD_URL, IDENTITY_URL } from "CommonUI/src/Config";
import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg";
import Navigation from "CommonUI/src/Utils/Navigation";
import UserUtil from "CommonUI/src/Utils/User";
import User from "Model/Models/User";
import React, { useState } from "react";
import ProjectSSO from "Model/Models/ProjectSSO";
import ErrorMessage from "CommonUI/src/Components/ErrorMessage/ErrorMessage";
import PageLoader from "CommonUI/src/Components/Loader/PageLoader";
import API from "CommonUI/src/Utils/API/API";
import BasicForm from "CommonUI/src/Components/Forms/BasicForm";
import Email from "Common/Types/Email";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import StaticModelList from "CommonUI/src/Components/ModelList/StaticModelList";
const LoginPage: () => JSX.Element = () => {
const apiUrl: URL = SERVICE_PROVIDER_LOGIN_URL;
if (UserUtil.isLoggedIn()) {
Navigation.navigate(DASHBOARD_URL);
}
const [error, setError] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [projectSsoConfigList, setProjectSsoConfigList] = useState<Array<ProjectSSO>>([]);
const fetchSsoConfigs = async (email: Email) => {
if (email) {
setIsLoading(true);
try {
// get sso config by email.
const listResult: HTTPErrorResponse | HTTPResponse<JSONArray> = await API.get(URL.fromString(apiUrl.toString()).addQueryParam("email", email.toString()));
if (listResult instanceof HTTPErrorResponse) {
throw listResult;
}
if (!listResult.data || (listResult.data as JSONArray).length === 0) {
return setError("No SSO configuration found for the email: " + email.toString());
}
setProjectSsoConfigList(ProjectSSO.fromJSONArray(listResult['data'], ProjectSSO));
} catch (error) {
setError(API.getFriendlyErrorMessage(error as Error));
}
setIsLoading(false);
} else {
setError("Email is required to perform this action");
}
};
const getSsoConfigModelList = (configs: Array<ProjectSSO>) => {
return (<StaticModelList<ProjectSSO>
list={configs}
titleField="name"
selectedItems={[]}
descriptionField="description"
onClick={(item: ProjectSSO) => {
setIsLoading(true);
Navigation.navigate(
URL.fromURL(IDENTITY_URL).addRoute(
new Route(
`/sso/${item.projectId?.toString()}/${item.id?.toString()
}`,
),
),
);
}}
/>);
}
if (error) {
return <ErrorMessage error={error} />;
}
if (isLoading) {
return <PageLoader isVisible={true} />;
}
const getProjectName = (projectId: string): string => {
const projectNames = projectSsoConfigList.filter((config: ProjectSSO) => config.projectId?.toString() === projectId.toString()).map((config: ProjectSSO) => config.project?.name);
return projectNames[0] || 'Project';
}
if (projectSsoConfigList.length > 0) {
const projectIds: Array<string> = projectSsoConfigList.map((config: ProjectSSO) => config.projectId?.toString() as string);
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">
Select Project
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Select the project you want to login to.
</p>
</div>
{projectIds.map((projectId: string) => {
return (
<div key={projectId}>
<h3 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
Project: {getProjectName(projectId)}
</h3>
{getSsoConfigModelList(projectSsoConfigList.filter((config: ProjectSSO) => config.projectId?.toString() === projectId.toString()))}
</div>
)
})}
</div>
);
}
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">
Login with SSO
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Login with your SSO provider to access your account.
</p>
</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">
<BasicForm
modelType={User}
id="login-form"
name="Login"
fields={[
{
field: {
email: true,
},
fieldType: FormFieldSchemaType.Email,
placeholder: "jeff@example.com",
required: true,
title: "Email",
dataTestId: "email",
}
]}
maxPrimaryButtonWidth={true}
submitButtonText="Login with SSO"
onSubmit={async (data: JSONObject) => {
await fetchSsoConfigs(data['email'] as Email);
}}
footer={
<div className="actions text-center mt-4 hover:underline fw-semibold">
<div>
<Link to={new Route("/accounts/login")}>
<div
className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm"
>
Use username and password insead.
</div>
</Link>
</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;

View File

@@ -9,6 +9,10 @@ export const LOGIN_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route("/login"),
);
export const SERVICE_PROVIDER_LOGIN_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route("/service-provider-login"),
);
export const FORGOT_PASSWORD_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route("/forgot-password"),
);

View File

@@ -5,6 +5,7 @@ import Hostname from "Common/Types/API/Hostname";
import Protocol from "Common/Types/API/Protocol";
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import OneUptimeDate from "Common/Types/Date";
import Email from "Common/Types/Email";
import BadRequestException from "Common/Types/Exception/BadRequestException";
@@ -19,6 +20,7 @@ import AccessTokenService from "CommonServer/Services/AccessTokenService";
import ProjectSSOService from "CommonServer/Services/ProjectSsoService";
import TeamMemberService from "CommonServer/Services/TeamMemberService";
import UserService from "CommonServer/Services/UserService";
import QueryHelper from "CommonServer/Types/Database/QueryHelper";
import CookieUtil from "CommonServer/Utils/Cookie";
import Express, {
ExpressRequest,
@@ -36,6 +38,83 @@ import xml2js from "xml2js";
const router: ExpressRouter = Express.getRouter();
// This route is used to get the SSO config for the user.
// when the user logs in from OneUptime and not from the IDP.
router.get("/service-provider-login", async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
if (!req.query['email']) {
return Response.sendErrorResponse(req, res, new BadRequestException("Email is required"));
}
const email: Email = new Email(req.query['email'] as string);
if (!email) {
return Response.sendErrorResponse(req, res, new BadRequestException("Email is required"));
}
// get sso config for this user.
const user: User | null = await UserService.findOneBy({
query: { email: email },
select: {
_id: true,
},
props: {
isRoot: true,
},
});
if (!user) {
return Response.sendErrorResponse(req, res, new BadRequestException("No SSO config found for this user"));
}
const userId: ObjectID = user.id!;
if (!userId) {
return Response.sendErrorResponse(req, res, new BadRequestException("No SSO config found for this user"));
}
const projectUserBelongsTo: Array<ObjectID> = (await TeamMemberService.findBy({
query: { userId: userId },
select: {
projectId: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
})).map((teamMember: TeamMember) => teamMember.projectId!);
if (projectUserBelongsTo.length === 0) {
return Response.sendErrorResponse(req, res, new BadRequestException("No SSO config found for this user"));
}
const projectSSOList: Array<ProjectSSO> = await ProjectSSOService.findBy({
query: { projectId: QueryHelper.any(projectUserBelongsTo), isEnabled: true },
limit: LIMIT_PER_PROJECT,
skip: 0,
select: {
name: true,
description: true,
_id: true,
projectId: true,
project: {
name: true,
},
},
props: {
isRoot: true,
},
});
return Response.sendEntityArrayResponse(req, res, projectSSOList, projectSSOList.length, ProjectSSO);
});
router.get(
"/sso/:projectId/:projectSsoId",
async (
@@ -262,9 +341,9 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
if (projectSSO.issuerURL.toString() !== issuerUrl) {
logger.error(
"Issuer URL does not match. It should be " +
projectSSO.issuerURL.toString() +
" but it is " +
issuerUrl.toString(),
projectSSO.issuerURL.toString() +
" but it is " +
issuerUrl.toString(),
);
return Response.sendErrorResponse(
req,