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 (
+
+
+

+
+ 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 (
+
+
+

+
+ 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,