From a06a0f1d166e28c0c9ee2b2a37f3baa0e5a74c91 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Mon, 17 Jun 2024 21:01:44 +0100 Subject: [PATCH] refactor: Update import statements for TimezoneUtil in multiple files --- .gitignore | 2 + Accounts/src/App.tsx | 3 + Accounts/src/Pages/Login.tsx | 32 +---- Accounts/src/Pages/LoginWithSSO.tsx | 206 ++++++++++++++++++++++++++++ Accounts/src/Utils/ApiPaths.ts | 4 + App/FeatureSet/Identity/API/SSO.ts | 85 +++++++++++- 6 files changed, 300 insertions(+), 32 deletions(-) create mode 100644 Accounts/src/Pages/LoginWithSSO.tsx diff --git a/.gitignore b/.gitignore index e372810579..cd106161be 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,8 @@ Llama/Models/llama* Llama/__pycache__/* +Llama/Models/* + Examples/otel-dotnet/obj/* InfrastructureAgent/sea-prep.blob diff --git a/Accounts/src/App.tsx b/Accounts/src/App.tsx index 25b0b0ead3..41102f653c 100644 --- a/Accounts/src/App.tsx +++ b/Accounts/src/App.tsx @@ -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 { } /> } /> + + } /> } diff --git a/Accounts/src/Pages/Login.tsx b/Accounts/src/Pages/Login.tsx index 376756e27d..3bf35f0268 100644 --- a/Accounts/src/Pages/Login.tsx +++ b/Accounts/src/Pages/Login.tsx @@ -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(false); const [initialValues, setInitialValues] = React.useState({}); @@ -56,16 +50,6 @@ const LoginPage: () => JSX.Element = () => {

- {showSsoMessage && ( -
- {" "} - {" "} -
- )} -
@@ -120,23 +104,13 @@ const LoginPage: () => JSX.Element = () => { footer={
- {!showSsoTip && ( +
{ - setShowSSOTip(true); - }} className="text-indigo-500 hover:text-indigo-900 cursor-pointer text-sm" > Use single sign-on (SSO) instead
- )} - - {showSsoTip && ( -
- Please sign in with your SSO provider like Okta, Auth0, - Entra ID or any other SAML 2.0 provider. -
- )} +
} diff --git a/Accounts/src/Pages/LoginWithSSO.tsx b/Accounts/src/Pages/LoginWithSSO.tsx new file mode 100644 index 0000000000..86d9918a4a --- /dev/null +++ b/Accounts/src/Pages/LoginWithSSO.tsx @@ -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(undefined); + const [isLoading, setIsLoading] = useState(false); + const [projectSsoConfigList, setProjectSsoConfigList] = useState>([]); + + const fetchSsoConfigs = async (email: Email) => { + if (email) { + setIsLoading(true); + try { + + // get sso config by email. + const listResult: HTTPErrorResponse | HTTPResponse = 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) => { + return ( + 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 ; + } + + if (isLoading) { + return ; + } + + 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 = projectSsoConfigList.map((config: ProjectSSO) => config.projectId?.toString() as string); + + return ( +
+
+ OneUptime +

+ Select Project +

+

+ Select the project you want to login to. +

+
+ + {projectIds.map((projectId: string) => { + return ( +
+

+ Project: {getProjectName(projectId)} +

+ {getSsoConfigModelList(projectSsoConfigList.filter((config: ProjectSSO) => config.projectId?.toString() === projectId.toString()))} +
+ ) + })} +
+ ); + + } + + + return ( +
+
+ OneUptime +

+ Login with SSO +

+

+ Login with your SSO provider to access your account. +

+
+ + + +
+
+ { + await fetchSsoConfigs(data['email'] as Email); + }} + footer={ +
+
+ +
+ Use username and password insead. +
+ +
+
+ } + /> +
+
+
+ Don't have an account?{" "} + + Register. + +
+
+
+
+ ); +}; + +export default LoginPage; diff --git a/Accounts/src/Utils/ApiPaths.ts b/Accounts/src/Utils/ApiPaths.ts index 8f9615e007..4c7a7ae860 100644 --- a/Accounts/src/Utils/ApiPaths.ts +++ b/Accounts/src/Utils/ApiPaths.ts @@ -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"), ); diff --git a/App/FeatureSet/Identity/API/SSO.ts b/App/FeatureSet/Identity/API/SSO.ts index b401e4a5be..e498b982c2 100644 --- a/App/FeatureSet/Identity/API/SSO.ts +++ b/App/FeatureSet/Identity/API/SSO.ts @@ -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 => { + + 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 = (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 = 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,