mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
75 Commits
dropdown-l
...
8.0.5580
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3feab3d0 | ||
|
|
7864bbb87b | ||
|
|
d112d87b80 | ||
|
|
2f8fcabce4 | ||
|
|
0023560588 | ||
|
|
0bc14acde9 | ||
|
|
3f3956edd6 | ||
|
|
93755da2e8 | ||
|
|
0657222ea7 | ||
|
|
ca352826ca | ||
|
|
3cbd99042b | ||
|
|
2f102acdc2 | ||
|
|
b76811d152 | ||
|
|
2335935a3e | ||
|
|
c324fe03d3 | ||
|
|
d5bc83a5a1 | ||
|
|
e2baa449f5 | ||
|
|
51b88eb065 | ||
|
|
b0d95bb7df | ||
|
|
8bf8c891ab | ||
|
|
fcf919c70b | ||
|
|
f0f3d32d31 | ||
|
|
444e8f17b6 | ||
|
|
3aabf44b4e | ||
|
|
c11fcc3c8e | ||
|
|
52519c9af8 | ||
|
|
2483cf9499 | ||
|
|
634e21b13c | ||
|
|
aad933b9eb | ||
|
|
9356f2964e | ||
|
|
aae70ead3b | ||
|
|
8a482dce10 | ||
|
|
9fdf46889c | ||
|
|
40ca9dc04c | ||
|
|
74937f2208 | ||
|
|
c02ab56477 | ||
|
|
3f99b9680f | ||
|
|
b08c39037d | ||
|
|
f7cc3c00da | ||
|
|
ac4286935a | ||
|
|
90a0b2e4a8 | ||
|
|
9b22c48d27 | ||
|
|
9c9dad5da0 | ||
|
|
e986f74025 | ||
|
|
deb2e81b21 | ||
|
|
0f8b322892 | ||
|
|
23c7de3ecd | ||
|
|
ad144a6240 | ||
|
|
debfef0388 | ||
|
|
bb85c9f8c8 | ||
|
|
25ab1cdbf9 | ||
|
|
44b8a9ddc9 | ||
|
|
c388ff9550 | ||
|
|
321d1680e6 | ||
|
|
6c0e9f0fed | ||
|
|
99349ecb30 | ||
|
|
258bbbd9cf | ||
|
|
1094a07fc6 | ||
|
|
14a5671645 | ||
|
|
5a41c66953 | ||
|
|
af605fce4c | ||
|
|
f8ef6c69fe | ||
|
|
e1848f44f7 | ||
|
|
825bd39dda | ||
|
|
b99905dfe8 | ||
|
|
a4bf40a2c1 | ||
|
|
711998b048 | ||
|
|
132e044c07 | ||
|
|
8ecc307451 | ||
|
|
c85c29989f | ||
|
|
95726e0f21 | ||
|
|
adc15992e9 | ||
|
|
58d83a2a80 | ||
|
|
5461cd4502 | ||
|
|
478465a65b |
1
.github/workflows/test.telemetry.yaml
vendored
1
.github/workflows/test.telemetry.yaml
vendored
@@ -17,5 +17,6 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Telemetry && npm install && npm run test
|
||||
|
||||
|
||||
19
APIReference/package-lock.json
generated
19
APIReference/package-lock.json
generated
@@ -33,19 +33,18 @@
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
|
||||
19
Accounts/package-lock.json
generated
19
Accounts/package-lock.json
generated
@@ -37,19 +37,18 @@
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
|
||||
19
AdminDashboard/package-lock.json
generated
19
AdminDashboard/package-lock.json
generated
@@ -36,19 +36,18 @@
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "react-router-dom";
|
||||
import UserView from "./Pages/Users/View/Index";
|
||||
import UserDelete from "./Pages/Users/View/Delete";
|
||||
import UserSettings from "./Pages/Users/View/Settings";
|
||||
import ProjectView from "./Pages/Projects/View/Index";
|
||||
import ProjectDelete from "./Pages/Projects/View/Delete";
|
||||
|
||||
@@ -71,6 +72,11 @@ const App: () => JSX.Element = () => {
|
||||
element={<UserView />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.USER_SETTINGS]?.toString() || ""}
|
||||
element={<UserSettings />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.USER_DELETE]?.toString() || ""}
|
||||
element={<UserDelete />}
|
||||
|
||||
94
AdminDashboard/src/Pages/Users/View/Settings.tsx
Normal file
94
AdminDashboard/src/Pages/Users/View/Settings.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import SideMenuComponent from "./SideMenu";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import ModelPage from "Common/UI/Components/Page/ModelPage";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
|
||||
const UserSettings: FunctionComponent = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<ModelPage<User>
|
||||
modelId={modelId}
|
||||
modelNameField="email"
|
||||
modelType={User}
|
||||
title={"User"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: "Admin Dashboard",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route),
|
||||
},
|
||||
{
|
||||
title: "User",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_VIEW] as Route,
|
||||
{
|
||||
modelId: modelId,
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_SETTINGS] as Route,
|
||||
{
|
||||
modelId: modelId,
|
||||
},
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenuComponent modelId={modelId} />}
|
||||
>
|
||||
<CardModelDetail<User>
|
||||
name="user-master-admin-settings"
|
||||
cardProps={{
|
||||
title: "Master Admin Access",
|
||||
description:
|
||||
"Grant or revoke master admin access for this user. Master admins can manage every project and workspace.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Update Access"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
isMasterAdmin: true,
|
||||
},
|
||||
title: "Master Admin",
|
||||
description:
|
||||
"Enable to give this user full access to the entire platform.",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: User,
|
||||
id: "user-master-admin-settings-detail",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
isMasterAdmin: true,
|
||||
},
|
||||
title: "Master Admin",
|
||||
fieldType: FieldType.Boolean,
|
||||
placeholder: "No",
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSettings;
|
||||
@@ -30,6 +30,18 @@ const SideMenuComponent: FunctionComponent<SideMenuProps> = (
|
||||
}}
|
||||
icon={IconProp.Info}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_SETTINGS] as Route,
|
||||
{
|
||||
modelId: props.modelId,
|
||||
},
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Settings}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
|
||||
@@ -6,6 +6,7 @@ enum PageMap {
|
||||
|
||||
USERS = "USERS",
|
||||
USER_VIEW = "USER_VIEW",
|
||||
USER_SETTINGS = "USER_SETTINGS",
|
||||
USER_DELETE = "USER_DELETE",
|
||||
|
||||
PROJECTS = "PROJECTS",
|
||||
|
||||
@@ -18,6 +18,9 @@ const RouteMap: Dictionary<Route> = {
|
||||
|
||||
[PageMap.USERS]: new Route(`/admin/users`),
|
||||
[PageMap.USER_VIEW]: new Route(`/admin/users/${RouteParams.ModelID}`),
|
||||
[PageMap.USER_SETTINGS]: new Route(
|
||||
`/admin/users/${RouteParams.ModelID}/settings`,
|
||||
),
|
||||
[PageMap.USER_DELETE]: new Route(
|
||||
`/admin/users/${RouteParams.ModelID}/delete`,
|
||||
),
|
||||
|
||||
@@ -25,24 +25,70 @@ import EmailVerificationTokenService from "Common/Server/Services/EmailVerificat
|
||||
import MailService from "Common/Server/Services/MailService";
|
||||
import UserService from "Common/Server/Services/UserService";
|
||||
import UserTotpAuthService from "Common/Server/Services/UserTotpAuthService";
|
||||
import UserSessionService, {
|
||||
SessionMetadata,
|
||||
} from "Common/Server/Services/UserSessionService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
extractDeviceInfo,
|
||||
getClientIp,
|
||||
headerValueToString,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import TotpAuth from "Common/Server/Utils/TotpAuth";
|
||||
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import UserSession from "Common/Models/DatabaseModels/UserSession";
|
||||
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
|
||||
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
|
||||
import UserWebAuthnService from "Common/Server/Services/UserWebAuthnService";
|
||||
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
type FinalizeUserLoginInput = {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
user: User;
|
||||
isGlobalLogin: boolean;
|
||||
};
|
||||
|
||||
const finalizeUserLogin: (
|
||||
data: FinalizeUserLoginInput,
|
||||
) => Promise<SessionMetadata> = async (
|
||||
data: FinalizeUserLoginInput,
|
||||
): Promise<SessionMetadata> => {
|
||||
const { req, res, user, isGlobalLogin } = data;
|
||||
|
||||
const sessionMetadata: SessionMetadata =
|
||||
await UserSessionService.createSession({
|
||||
userId: user.id!,
|
||||
isGlobalLogin,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
});
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
user,
|
||||
isGlobalLogin,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
refreshToken: sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return sessionMetadata;
|
||||
};
|
||||
|
||||
router.post(
|
||||
"/signup",
|
||||
async (
|
||||
@@ -185,9 +231,9 @@ router.post(
|
||||
if (savedUser) {
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(savedUser.id!);
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
await finalizeUserLogin({
|
||||
req,
|
||||
res,
|
||||
user: savedUser,
|
||||
isGlobalLogin: true,
|
||||
});
|
||||
@@ -487,6 +533,127 @@ router.post(
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/refresh-token",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req);
|
||||
|
||||
if (!refreshToken) {
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException(
|
||||
"Refresh token missing. Please login again.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const session: UserSession | null =
|
||||
await UserSessionService.findActiveSessionByRefreshToken(refreshToken);
|
||||
|
||||
if (!session || !session.id) {
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
session.refreshTokenExpiresAt &&
|
||||
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
|
||||
) {
|
||||
await UserSessionService.revokeSessionById(session.id, {
|
||||
reason: "Refresh token expired",
|
||||
});
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.userId) {
|
||||
await UserSessionService.revokeSessionById(session.id, {
|
||||
reason: "Session missing user",
|
||||
});
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: session.userId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
isMasterAdmin: true,
|
||||
profilePictureId: true,
|
||||
timezone: true,
|
||||
enableTwoFactorAuth: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
await UserSessionService.revokeSessionById(session.id, {
|
||||
reason: "User not found",
|
||||
});
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Account no longer exists."),
|
||||
);
|
||||
}
|
||||
|
||||
const additionalInfo: JSONObject = (session.additionalInfo ||
|
||||
{}) as JSONObject;
|
||||
const isGlobalLogin: boolean =
|
||||
typeof additionalInfo["isGlobalLogin"] === "boolean"
|
||||
? (additionalInfo["isGlobalLogin"] as boolean)
|
||||
: true;
|
||||
|
||||
const renewedSession: SessionMetadata =
|
||||
await UserSessionService.renewSessionWithNewRefreshToken({
|
||||
session,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
});
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
user,
|
||||
isGlobalLogin,
|
||||
sessionId: renewedSession.session.id!,
|
||||
refreshToken: renewedSession.refreshToken,
|
||||
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/logout",
|
||||
async (
|
||||
@@ -495,6 +662,15 @@ router.post(
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req);
|
||||
|
||||
if (refreshToken) {
|
||||
await UserSessionService.revokeSessionByRefreshToken(refreshToken, {
|
||||
reason: "User logout",
|
||||
});
|
||||
}
|
||||
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
@@ -788,8 +964,9 @@ const login: LoginFunction = async (options: {
|
||||
if (alreadySavedUser.password.toString() === user.password!.toString()) {
|
||||
logger.info("User logged in: " + alreadySavedUser.email?.toString());
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
await finalizeUserLogin({
|
||||
req,
|
||||
res,
|
||||
user: alreadySavedUser,
|
||||
isGlobalLogin: true,
|
||||
});
|
||||
|
||||
@@ -20,6 +20,9 @@ import AccessTokenService from "Common/Server/Services/AccessTokenService";
|
||||
import ProjectSSOService from "Common/Server/Services/ProjectSsoService";
|
||||
import TeamMemberService from "Common/Server/Services/TeamMemberService";
|
||||
import UserService from "Common/Server/Services/UserService";
|
||||
import UserSessionService, {
|
||||
SessionMetadata,
|
||||
} from "Common/Server/Services/UserSessionService";
|
||||
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
|
||||
import Select from "Common/Server/Types/Database/Select";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
@@ -28,6 +31,9 @@ import Express, {
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
extractDeviceInfo,
|
||||
getClientIp,
|
||||
headerValueToString,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -40,6 +46,8 @@ import Name from "Common/Types/Name";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
/*
|
||||
* This route is used to get the SSO config for the user.
|
||||
* when the user logs in from OneUptime and not from the IDP.
|
||||
@@ -539,15 +547,31 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
expressResponse: res,
|
||||
});
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
|
||||
|
||||
const sessionMetadata: SessionMetadata =
|
||||
await UserSessionService.createSession({
|
||||
userId: alreadySavedUser.id!,
|
||||
isGlobalLogin: false,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
additionalInfo: {
|
||||
projectId: projectId.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
user: alreadySavedUser,
|
||||
isGlobalLogin: false,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
refreshToken: sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
|
||||
@@ -6,30 +6,93 @@ import URL from "Common/Types/API/URL";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import JSONFunctions from "Common/Types/JSONFunctions";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import PositiveNumber from "Common/Types/PositiveNumber";
|
||||
import DatabaseConfig from "Common/Server/DatabaseConfig";
|
||||
import { EncryptionSecret } from "Common/Server/EnvironmentConfig";
|
||||
import MailService from "Common/Server/Services/MailService";
|
||||
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
|
||||
import StatusPageService from "Common/Server/Services/StatusPageService";
|
||||
import StatusPagePrivateUserSessionService, {
|
||||
SessionMetadata as StatusPageSessionMetadata,
|
||||
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
extractDeviceInfo,
|
||||
getClientIp,
|
||||
headerValueToString,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
|
||||
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
|
||||
import StatusPagePrivateUserSession from "Common/Models/DatabaseModels/StatusPagePrivateUserSession";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
type FinalizeStatusPageLoginInput = {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
user: StatusPagePrivateUser;
|
||||
};
|
||||
|
||||
const finalizeStatusPageLogin: (data: FinalizeStatusPageLoginInput) => Promise<{
|
||||
sessionMetadata: StatusPageSessionMetadata;
|
||||
accessToken: string;
|
||||
}> = async (
|
||||
data: FinalizeStatusPageLoginInput,
|
||||
): Promise<{
|
||||
sessionMetadata: StatusPageSessionMetadata;
|
||||
accessToken: string;
|
||||
}> => {
|
||||
const { req, res, user } = data;
|
||||
|
||||
if (!user.projectId) {
|
||||
throw new BadDataException(
|
||||
"Status page user is missing associated projectId.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.statusPageId) {
|
||||
throw new BadDataException(
|
||||
"Status page user is missing associated statusPageId.",
|
||||
);
|
||||
}
|
||||
|
||||
const sessionMetadata: StatusPageSessionMetadata =
|
||||
await StatusPagePrivateUserSessionService.createSession({
|
||||
projectId: user.projectId,
|
||||
statusPageId: user.statusPageId,
|
||||
statusPagePrivateUserId: user.id!,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
});
|
||||
|
||||
const accessToken: string = CookieUtil.setStatusPagePrivateUserCookie({
|
||||
expressResponse: res,
|
||||
user,
|
||||
statusPageId: user.statusPageId,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
refreshToken: sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return {
|
||||
sessionMetadata,
|
||||
accessToken,
|
||||
};
|
||||
};
|
||||
|
||||
router.post(
|
||||
"/logout/:statuspageid",
|
||||
async (
|
||||
@@ -46,7 +109,20 @@ router.post(
|
||||
req.params["statuspageid"].toString(),
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId)); // remove the cookie.
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req, statusPageId);
|
||||
|
||||
if (refreshToken) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionByRefreshToken(
|
||||
refreshToken,
|
||||
{
|
||||
reason: "User logged out",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(res, CookieUtil.getRefreshTokenKey(statusPageId));
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
@@ -55,6 +131,198 @@ router.post(
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/refresh-token/:statuspageid",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const statusPageIdParam: string | undefined = req.params["statuspageid"];
|
||||
|
||||
if (!statusPageIdParam) {
|
||||
throw new BadDataException("Status Page ID is required.");
|
||||
}
|
||||
|
||||
const statusPageId: ObjectID = new ObjectID(statusPageIdParam.toString());
|
||||
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req, statusPageId);
|
||||
|
||||
if (!refreshToken) {
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException(
|
||||
"Refresh token missing. Please login again.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const session: StatusPagePrivateUserSession | null =
|
||||
await StatusPagePrivateUserSessionService.findActiveSessionByRefreshToken(
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
if (!session || !session.id || !session.statusPageId) {
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (session.statusPageId.toString() !== statusPageId.toString()) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionById(
|
||||
session.id,
|
||||
{
|
||||
reason: "Status page mismatch",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
session.refreshTokenExpiresAt &&
|
||||
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
|
||||
) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionById(
|
||||
session.id,
|
||||
{
|
||||
reason: "Refresh token expired",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.statusPagePrivateUserId) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionById(
|
||||
session.id,
|
||||
{
|
||||
reason: "Session missing user",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
const user: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneById({
|
||||
id: session.statusPagePrivateUserId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
statusPageId: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionById(
|
||||
session.id,
|
||||
{
|
||||
reason: "User not found",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Account no longer exists."),
|
||||
);
|
||||
}
|
||||
|
||||
const renewedSession: StatusPageSessionMetadata =
|
||||
await StatusPagePrivateUserSessionService.renewSessionWithNewRefreshToken(
|
||||
{
|
||||
session,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
},
|
||||
);
|
||||
|
||||
const accessToken: string = CookieUtil.setStatusPagePrivateUserCookie({
|
||||
expressResponse: res,
|
||||
user,
|
||||
statusPageId: user.statusPageId!,
|
||||
sessionId: renewedSession.session.id!,
|
||||
refreshToken: renewedSession.refreshToken,
|
||||
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return Response.sendEntityResponse(
|
||||
req,
|
||||
res,
|
||||
user,
|
||||
StatusPagePrivateUser,
|
||||
{
|
||||
miscData: {
|
||||
token: accessToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/forgot-password",
|
||||
async (
|
||||
@@ -376,6 +644,7 @@ router.post(
|
||||
password: true,
|
||||
email: true,
|
||||
statusPageId: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -383,31 +652,38 @@ router.post(
|
||||
});
|
||||
|
||||
if (alreadySavedUser) {
|
||||
const token: string = JSONWebToken.sign({
|
||||
data: alreadySavedUser,
|
||||
expiresInSeconds: OneUptimeDate.getSecondsInDays(
|
||||
new PositiveNumber(30),
|
||||
),
|
||||
const { accessToken } = await finalizeStatusPageLogin({
|
||||
req,
|
||||
res,
|
||||
user: alreadySavedUser,
|
||||
});
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
|
||||
token,
|
||||
{
|
||||
httpOnly: true,
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
},
|
||||
);
|
||||
const sanitizedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneById({
|
||||
id: alreadySavedUser.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
statusPageId: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!sanitizedUser && (alreadySavedUser as any).password) {
|
||||
delete (alreadySavedUser as any).password;
|
||||
}
|
||||
|
||||
return Response.sendEntityResponse(
|
||||
req,
|
||||
res,
|
||||
alreadySavedUser,
|
||||
sanitizedUser || alreadySavedUser,
|
||||
StatusPagePrivateUser,
|
||||
{
|
||||
miscData: {
|
||||
token: token,
|
||||
token: accessToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import SSOUtil from "../Utils/SSO";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import Email from "Common/Types/Email";
|
||||
import BadRequestException from "Common/Types/Exception/BadRequestException";
|
||||
import Exception from "Common/Types/Exception/Exception";
|
||||
@@ -8,9 +7,11 @@ import ServerException from "Common/Types/Exception/ServerException";
|
||||
import HashedString from "Common/Types/HashedString";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import PositiveNumber from "Common/Types/PositiveNumber";
|
||||
import { Host, HttpProtocol } from "Common/Server/EnvironmentConfig";
|
||||
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
|
||||
import StatusPagePrivateUserSessionService, {
|
||||
SessionMetadata as StatusPageSessionMetadata,
|
||||
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
|
||||
import StatusPageService from "Common/Server/Services/StatusPageService";
|
||||
import StatusPageSsoService from "Common/Server/Services/StatusPageSsoService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
@@ -19,8 +20,10 @@ import Express, {
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
extractDeviceInfo,
|
||||
getClientIp,
|
||||
headerValueToString,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
|
||||
@@ -30,6 +33,8 @@ import xml2js from "xml2js";
|
||||
// Initialize Express router.
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
// Define a GET route for SSO in a status page context.
|
||||
router.get(
|
||||
"/status-page-sso/:statusPageId/:statusPageSsoId",
|
||||
@@ -285,24 +290,30 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const token: string = JSONWebToken.sign({
|
||||
data: alreadySavedUser,
|
||||
expiresInSeconds: OneUptimeDate.getSecondsInDays(
|
||||
new PositiveNumber(30),
|
||||
),
|
||||
if (!alreadySavedUser.projectId) {
|
||||
alreadySavedUser.projectId = projectId;
|
||||
}
|
||||
|
||||
const sessionMetadata: StatusPageSessionMetadata =
|
||||
await StatusPagePrivateUserSessionService.createSession({
|
||||
projectId: alreadySavedUser.projectId!,
|
||||
statusPageId: statusPageId,
|
||||
statusPagePrivateUserId: alreadySavedUser.id!,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
});
|
||||
|
||||
const token: string = CookieUtil.setStatusPagePrivateUserCookie({
|
||||
expressResponse: res,
|
||||
user: alreadySavedUser,
|
||||
statusPageId: statusPageId,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
refreshToken: sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
19
App/package-lock.json
generated
19
App/package-lock.json
generated
@@ -43,19 +43,18 @@
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
|
||||
@@ -10,6 +10,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
@@ -418,6 +419,7 @@ export default class AlertFeed extends BaseModel {
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -378,6 +379,7 @@ export default class AlertSeverity extends BaseModel {
|
||||
Permission.EditAlertSeverity,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -356,6 +357,7 @@ export default class AlertState extends BaseModel {
|
||||
Permission.EditAlertState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -15,6 +15,7 @@ import TableColumn, {
|
||||
getTableColumns,
|
||||
} from "../../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../../Types/Database/TableColumnType";
|
||||
import { getFirstColorFieldColumn } from "../../../Types/Database/ColorField";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import Email from "../../../Types/Email";
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
@@ -203,6 +204,10 @@ export default class DatabaseBaseModel extends BaseEntity {
|
||||
return new Columns(Object.keys(getTableColumns(this)));
|
||||
}
|
||||
|
||||
public getFirstColorColumn(): string | null {
|
||||
return getFirstColorFieldColumn(this);
|
||||
}
|
||||
|
||||
public canQueryMultiTenant(): boolean {
|
||||
return Boolean(this.isMultiTenantRequestAllowed);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -378,6 +379,7 @@ export default class IncidentSeverity extends BaseModel {
|
||||
Permission.EditIncidentSeverity,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -380,6 +381,7 @@ export default class IncidentState extends BaseModel {
|
||||
Permission.EditIncidentState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -118,6 +118,7 @@ import StatusPageHistoryChartBarColorRule from "./StatusPageHistoryChartBarColor
|
||||
import StatusPageOwnerTeam from "./StatusPageOwnerTeam";
|
||||
import StatusPageOwnerUser from "./StatusPageOwnerUser";
|
||||
import StatusPagePrivateUser from "./StatusPagePrivateUser";
|
||||
import StatusPagePrivateUserSession from "./StatusPagePrivateUserSession";
|
||||
import StatusPageResource from "./StatusPageResource";
|
||||
import StatusPageSCIM from "./StatusPageSCIM";
|
||||
import StatusPageSSO from "./StatusPageSso";
|
||||
@@ -130,6 +131,7 @@ import TeamComplianceSetting from "./TeamComplianceSetting";
|
||||
import TelemetryService from "./TelemetryService";
|
||||
import UsageBilling from "./TelemetryUsageBilling";
|
||||
import User from "./User";
|
||||
import UserSession from "./UserSession";
|
||||
import UserCall from "./UserCall";
|
||||
// Notification Methods
|
||||
import UserEmail from "./UserEmail";
|
||||
@@ -266,6 +268,7 @@ const AllModelTypes: Array<{
|
||||
StatusPageFooterLink,
|
||||
StatusPageHeaderLink,
|
||||
StatusPagePrivateUser,
|
||||
StatusPagePrivateUserSession,
|
||||
StatusPageHistoryChartBarColorRule,
|
||||
|
||||
ScheduledMaintenanceState,
|
||||
@@ -375,6 +378,7 @@ const AllModelTypes: Array<{
|
||||
ProbeOwnerTeam,
|
||||
ProbeOwnerUser,
|
||||
|
||||
UserSession,
|
||||
UserTotpAuth,
|
||||
UserWebAuthn,
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -365,6 +366,7 @@ export default class Label extends AccessControlModel {
|
||||
Permission.EditProjectLabel,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -379,6 +380,7 @@ export default class MonitorStatus extends BaseModel {
|
||||
Permission.EditProjectMonitorStatus,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -10,6 +10,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
@@ -422,6 +423,7 @@ export default class ScheduledMaintenanceFeed extends BaseModel {
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -380,6 +381,7 @@ export default class ScheduledMaintenanceState extends BaseModel {
|
||||
Permission.EditScheduledMaintenanceState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -14,6 +14,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -448,6 +449,7 @@ export default class ServiceCatalog extends BaseModel {
|
||||
Permission.EditServiceCatalog,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
title: "Service Color",
|
||||
|
||||
413
Common/Models/DatabaseModels/StatusPagePrivateUserSession.ts
Normal file
413
Common/Models/DatabaseModels/StatusPagePrivateUserSession.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Project from "./Project";
|
||||
import StatusPage from "./StatusPage";
|
||||
import StatusPagePrivateUser from "./StatusPagePrivateUser";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
|
||||
import CanAccessIfCanReadOn from "../../Types/Database/CanAccessIfCanReadOn";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableBillingAccessControl({
|
||||
create: PlanType.Growth,
|
||||
read: PlanType.Growth,
|
||||
update: PlanType.Growth,
|
||||
delete: PlanType.Growth,
|
||||
})
|
||||
@CanAccessIfCanReadOn("statusPage")
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/status-page-private-user-session"))
|
||||
@Entity({
|
||||
name: "StatusPagePrivateUserSession",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "StatusPagePrivateUserSession",
|
||||
singularName: "Status Page Private User Session",
|
||||
pluralName: "Status Page Private User Sessions",
|
||||
icon: IconProp.Lock,
|
||||
tableDescription:
|
||||
"Stores status page private user sessions, refresh tokens, and device metadata for secure access control.",
|
||||
})
|
||||
export default class StatusPagePrivateUserSession extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Project that owns this private status page session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Project ID",
|
||||
description: "Project identifier for this session.",
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPage,
|
||||
title: "Status Page",
|
||||
description: "Status page associated with this session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPage;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPageId" })
|
||||
public statusPage?: StatusPage = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Status Page ID",
|
||||
description: "Identifier for the status page.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPagePrivateUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPagePrivateUser,
|
||||
title: "Status Page Private User",
|
||||
description: "Private user record associated with this session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPagePrivateUser;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPagePrivateUserId" })
|
||||
public statusPagePrivateUser?: StatusPagePrivateUser = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Status Page Private User ID",
|
||||
description: "Identifier for the status page private user.",
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPagePrivateUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index({ unique: true })
|
||||
@TableColumn({
|
||||
type: TableColumnType.HashedString,
|
||||
title: "Refresh Token",
|
||||
description: "Hashed refresh token for the private user session.",
|
||||
required: true,
|
||||
hideColumnInDocumentation: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.HashedString,
|
||||
length: ColumnLength.HashedString,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
transformer: HashedString.getDatabaseTransformer(),
|
||||
})
|
||||
public refreshToken?: HashedString = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Refresh Token Expires At",
|
||||
description: "Expiration timestamp for the refresh token.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
})
|
||||
public refreshTokenExpiresAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Last Active At",
|
||||
description: "Last time this session was active.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public lastActiveAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Name",
|
||||
description: "Friendly name for the device used to access the status page.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Type",
|
||||
description: "Type of device (desktop, mobile, etc).",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceType?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device OS",
|
||||
description: "Operating system reported for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceOS?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Browser",
|
||||
description: "Browser or client application used for the session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceBrowser?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "IP Address",
|
||||
description: "IP address recorded for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public ipAddress?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "User Agent",
|
||||
description: "User agent string supplied by the client.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
})
|
||||
public userAgent?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Revoked",
|
||||
description: "Indicates if the session has been revoked.",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isRevoked?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Revoked At",
|
||||
description: "Timestamp when the session was revoked, if applicable.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Revoked Reason",
|
||||
description: "Reason provided for revoking this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedReason?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.JSON,
|
||||
title: "Additional Info",
|
||||
description: "Flexible JSON payload for storing structured metadata.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public additionalInfo?: JSONObject = undefined;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -505,6 +506,7 @@ export default class TelemetryService extends BaseModel {
|
||||
Permission.EditTelemetryService,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
title: "Service Color",
|
||||
|
||||
318
Common/Models/DatabaseModels/UserSession.ts
Normal file
318
Common/Models/DatabaseModels/UserSession.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import User from "./User";
|
||||
import Route from "../../Types/API/Route";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation({
|
||||
isMasterAdminApiDocs: true,
|
||||
})
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-session"))
|
||||
@Entity({
|
||||
name: "UserSession",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserSession",
|
||||
singularName: "User Session",
|
||||
pluralName: "User Sessions",
|
||||
icon: IconProp.Lock,
|
||||
tableDescription:
|
||||
"Active user sessions with refresh tokens and device metadata for enhanced authentication security.",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserSession extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "User account this session belongs to.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "User ID",
|
||||
description: "Identifier for the user that owns this session.",
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index({ unique: true })
|
||||
@TableColumn({
|
||||
type: TableColumnType.HashedString,
|
||||
title: "Refresh Token",
|
||||
description: "Hashed refresh token for this session.",
|
||||
required: true,
|
||||
hideColumnInDocumentation: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.HashedString,
|
||||
length: ColumnLength.HashedString,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
transformer: HashedString.getDatabaseTransformer(),
|
||||
})
|
||||
public refreshToken?: HashedString = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Refresh Token Expires At",
|
||||
description: "Expiration timestamp for the refresh token.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
})
|
||||
public refreshTokenExpiresAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Last Active At",
|
||||
description: "Last time this session was used.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public lastActiveAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Name",
|
||||
description: "Friendly name for the device used to sign in.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Type",
|
||||
description: "Type of device (e.g., desktop, mobile).",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceType?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device OS",
|
||||
description: "Operating system reported for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceOS?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Browser",
|
||||
description: "Browser or client application used for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceBrowser?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "IP Address",
|
||||
description: "IP address observed for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public ipAddress?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "User Agent",
|
||||
description: "Complete user agent string supplied by the client.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
})
|
||||
public userAgent?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Revoked",
|
||||
description: "Marks whether the session has been explicitly revoked.",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isRevoked?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Revoked At",
|
||||
description: "Timestamp when the session was revoked, if applicable.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Revoked Reason",
|
||||
description: "Optional reason describing why the session was revoked.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedReason?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.JSON,
|
||||
title: "Additional Info",
|
||||
description:
|
||||
"Flexible JSON payload for storing structured session metadata.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public additionalInfo?: JSONObject = undefined;
|
||||
}
|
||||
|
||||
export default UserSession;
|
||||
@@ -0,0 +1,91 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1762890441920 implements MigrationInterface {
|
||||
public name = "MigrationName1762890441920";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "StatusPagePrivateUserSession" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "statusPageId" uuid NOT NULL, "statusPagePrivateUserId" uuid NOT NULL, "refreshToken" character varying(64) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastActiveAt" TIMESTAMP WITH TIME ZONE, "deviceName" character varying(100), "deviceType" character varying(100), "deviceOS" character varying(100), "deviceBrowser" character varying(100), "ipAddress" character varying(100), "userAgent" text, "isRevoked" boolean NOT NULL DEFAULT false, "revokedAt" TIMESTAMP WITH TIME ZONE, "revokedReason" character varying(100), "additionalInfo" jsonb, CONSTRAINT "UQ_12ce827a16d121bf6719260b8a9" UNIQUE ("refreshToken"), CONSTRAINT "PK_cbace84fe4c9712b94e571dc133" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ac5f4c13d6bc9696cbfb8e5a79" ON "StatusPagePrivateUserSession" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7b8d9b6e068c045d56b47a484b" ON "StatusPagePrivateUserSession" ("statusPageId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_365d602943505272f8f651ff4e" ON "StatusPagePrivateUserSession" ("statusPagePrivateUserId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_12ce827a16d121bf6719260b8a" ON "StatusPagePrivateUserSession" ("refreshToken") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "UserSession" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "userId" uuid NOT NULL, "refreshToken" character varying(64) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastActiveAt" TIMESTAMP WITH TIME ZONE, "deviceName" character varying(100), "deviceType" character varying(100), "deviceOS" character varying(100), "deviceBrowser" character varying(100), "ipAddress" character varying(100), "userAgent" text, "isRevoked" boolean NOT NULL DEFAULT false, "revokedAt" TIMESTAMP WITH TIME ZONE, "revokedReason" character varying(100), "additionalInfo" jsonb, CONSTRAINT "UQ_d66bd8342b0005c7192bdb17efc" UNIQUE ("refreshToken"), CONSTRAINT "PK_9dcd180f25755bab5fcebcbeb14" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7353eaf92987aeaf38c2590e94" ON "UserSession" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_d66bd8342b0005c7192bdb17ef" ON "UserSession" ("refreshToken") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_365d602943505272f8f651ff4e8" FOREIGN KEY ("statusPagePrivateUserId") REFERENCES "StatusPagePrivateUser"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" ADD CONSTRAINT "FK_7353eaf92987aeaf38c2590e943" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" DROP CONSTRAINT "FK_7353eaf92987aeaf38c2590e943"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_365d602943505272f8f651ff4e8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_d66bd8342b0005c7192bdb17ef"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7353eaf92987aeaf38c2590e94"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "UserSession"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_12ce827a16d121bf6719260b8a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_365d602943505272f8f651ff4e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7b8d9b6e068c045d56b47a484b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ac5f4c13d6bc9696cbfb8e5a79"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "StatusPagePrivateUserSession"`);
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,7 @@ import { MigrationName1761232578396 } from "./1761232578396-MigrationName";
|
||||
import { MigrationName1761834523183 } from "./1761834523183-MigrationName";
|
||||
import { MigrationName1762181014879 } from "./1762181014879-MigrationName";
|
||||
import { MigrationName1762554602716 } from "./1762554602716-MigrationName";
|
||||
import { MigrationName1762890441920 } from "./1762890441920-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -366,4 +367,5 @@ export default [
|
||||
MigrationName1761834523183,
|
||||
MigrationName1762181014879,
|
||||
MigrationName1762554602716,
|
||||
MigrationName1762890441920,
|
||||
];
|
||||
|
||||
@@ -106,6 +106,7 @@ import StatusPageHistoryChartBarColorRuleService from "./StatusPageHistoryChartB
|
||||
import StatusPageOwnerTeamService from "./StatusPageOwnerTeamService";
|
||||
import StatusPageOwnerUserService from "./StatusPageOwnerUserService";
|
||||
import StatusPagePrivateUserService from "./StatusPagePrivateUserService";
|
||||
import StatusPagePrivateUserSessionService from "./StatusPagePrivateUserSessionService";
|
||||
import StatusPageResourceService from "./StatusPageResourceService";
|
||||
// Status Page
|
||||
import StatusPageService from "./StatusPageService";
|
||||
@@ -125,6 +126,7 @@ import UserNotificationSettingService from "./UserNotificationSettingService";
|
||||
import UserOnCallLogService from "./UserOnCallLogService";
|
||||
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
|
||||
import UserService from "./UserService";
|
||||
import UserSessionService from "./UserSessionService";
|
||||
import UserTotpAuthService from "./UserTotpAuthService";
|
||||
import UserWebAuthnService from "./UserWebAuthnService";
|
||||
import UserSmsService from "./UserSmsService";
|
||||
@@ -266,6 +268,7 @@ const services: Array<BaseService> = [
|
||||
StatusPageOwnerTeamService,
|
||||
StatusPageOwnerUserService,
|
||||
StatusPagePrivateUserService,
|
||||
StatusPagePrivateUserSessionService,
|
||||
StatusPageResourceService,
|
||||
StatusPageService,
|
||||
StatusPageSsoService,
|
||||
@@ -278,6 +281,7 @@ const services: Array<BaseService> = [
|
||||
TeamService,
|
||||
|
||||
UserService,
|
||||
UserSessionService,
|
||||
UserCallService,
|
||||
UserEmailService,
|
||||
UserNotificationRuleService,
|
||||
|
||||
368
Common/Server/Services/StatusPagePrivateUserSessionService.ts
Normal file
368
Common/Server/Services/StatusPagePrivateUserSessionService.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/StatusPagePrivateUserSession";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import { EncryptionSecret } from "../EnvironmentConfig";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import Text from "../../Types/Text";
|
||||
import logger from "../Utils/Logger";
|
||||
import Exception from "../../Types/Exception/Exception";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
|
||||
export interface SessionMetadata {
|
||||
session: Model;
|
||||
refreshToken: string;
|
||||
refreshTokenExpiresAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateSessionOptions {
|
||||
projectId: ObjectID;
|
||||
statusPageId: ObjectID;
|
||||
statusPagePrivateUserId: ObjectID;
|
||||
refreshToken?: string | undefined;
|
||||
refreshTokenExpiresAt?: Date | undefined;
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
deviceName?: string | undefined;
|
||||
deviceType?: string | undefined;
|
||||
deviceOS?: string | undefined;
|
||||
deviceBrowser?: string | undefined;
|
||||
additionalInfo?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
export interface RenewSessionOptions {
|
||||
session: Model;
|
||||
refreshTokenExpiresAt?: Date | undefined;
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
deviceName?: string | undefined;
|
||||
deviceType?: string | undefined;
|
||||
deviceOS?: string | undefined;
|
||||
deviceBrowser?: string | undefined;
|
||||
additionalInfo?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
export interface TouchSessionOptions {
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
}
|
||||
|
||||
export interface RevokeSessionOptions {
|
||||
reason?: string | undefined;
|
||||
}
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
private static readonly DEFAULT_REFRESH_TOKEN_TTL_DAYS: number = 30;
|
||||
private static readonly SHORT_TEXT_LIMIT: number = 100;
|
||||
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
public async createSession(
|
||||
options: CreateSessionOptions,
|
||||
): Promise<SessionMetadata> {
|
||||
const refreshToken: string =
|
||||
options.refreshToken || Service.generateRefreshToken();
|
||||
const refreshTokenExpiresAt: Date =
|
||||
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
||||
|
||||
const session: Model = this.buildSessionModel(options, {
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
});
|
||||
|
||||
try {
|
||||
const createdSession: Model = await this.create({
|
||||
data: session,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
session: createdSession,
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
};
|
||||
} catch (error) {
|
||||
throw error as Exception;
|
||||
}
|
||||
}
|
||||
|
||||
public async findActiveSessionByRefreshToken(
|
||||
refreshToken: string,
|
||||
): Promise<Model | null> {
|
||||
const hashedValue: string = await HashedString.hashValue(
|
||||
refreshToken,
|
||||
EncryptionSecret,
|
||||
);
|
||||
|
||||
const session: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
refreshToken: new HashedString(hashedValue, true),
|
||||
isRevoked: false,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
statusPageId: true,
|
||||
statusPagePrivateUserId: true,
|
||||
refreshTokenExpiresAt: true,
|
||||
lastActiveAt: true,
|
||||
additionalInfo: true,
|
||||
deviceName: true,
|
||||
deviceType: true,
|
||||
deviceOS: true,
|
||||
deviceBrowser: true,
|
||||
ipAddress: true,
|
||||
userAgent: true,
|
||||
isRevoked: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!session.refreshTokenExpiresAt ||
|
||||
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public async renewSessionWithNewRefreshToken(
|
||||
options: RenewSessionOptions,
|
||||
): Promise<SessionMetadata> {
|
||||
const refreshToken: string = Service.generateRefreshToken();
|
||||
const refreshTokenExpiresAt: Date =
|
||||
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
||||
|
||||
const updatePayload: Partial<Model> = {
|
||||
refreshToken: HashedString.fromString(refreshToken),
|
||||
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
||||
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
||||
isRevoked: false,
|
||||
};
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
|
||||
if (ipAddress) {
|
||||
updatePayload.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
if (options.userAgent) {
|
||||
updatePayload.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
const deviceName: string | undefined = Text.truncate(
|
||||
options.deviceName,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceName) {
|
||||
updatePayload.deviceName = deviceName;
|
||||
}
|
||||
|
||||
const deviceType: string | undefined = Text.truncate(
|
||||
options.deviceType,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceType) {
|
||||
updatePayload.deviceType = deviceType;
|
||||
}
|
||||
|
||||
const deviceOS: string | undefined = Text.truncate(
|
||||
options.deviceOS,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceOS) {
|
||||
updatePayload.deviceOS = deviceOS;
|
||||
}
|
||||
|
||||
const deviceBrowser: string | undefined = Text.truncate(
|
||||
options.deviceBrowser,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceBrowser) {
|
||||
updatePayload.deviceBrowser = deviceBrowser;
|
||||
}
|
||||
|
||||
if (options.additionalInfo || options.session.additionalInfo) {
|
||||
updatePayload.additionalInfo = {
|
||||
...(options.session.additionalInfo || {}),
|
||||
...(options.additionalInfo || {}),
|
||||
} as JSONObject;
|
||||
}
|
||||
|
||||
const updatedSession: Model | null = await this.updateOneByIdAndFetch({
|
||||
id: options.session.id!,
|
||||
data: updatePayload as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updatedSession) {
|
||||
throw new BadDataException("Unable to renew status page user session");
|
||||
}
|
||||
|
||||
return {
|
||||
session: updatedSession,
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
public async touchSession(
|
||||
sessionId: ObjectID,
|
||||
options: TouchSessionOptions,
|
||||
): Promise<void> {
|
||||
const updatePayload: Partial<Model> = {
|
||||
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
||||
};
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
|
||||
if (ipAddress) {
|
||||
updatePayload.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
if (options.userAgent) {
|
||||
updatePayload.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.updateOneById({
|
||||
id: sessionId,
|
||||
data: updatePayload as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Failed to update status page session activity for session ${sessionId.toString()}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async revokeSessionById(
|
||||
sessionId: ObjectID,
|
||||
options?: RevokeSessionOptions,
|
||||
): Promise<void> {
|
||||
await this.updateOneById({
|
||||
id: sessionId,
|
||||
data: {
|
||||
isRevoked: true,
|
||||
revokedAt: OneUptimeDate.getCurrentDate(),
|
||||
revokedReason: options?.reason ?? null,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async revokeSessionByRefreshToken(
|
||||
refreshToken: string,
|
||||
options?: RevokeSessionOptions,
|
||||
): Promise<void> {
|
||||
const session: Model | null =
|
||||
await this.findActiveSessionByRefreshToken(refreshToken);
|
||||
|
||||
if (!session || !session.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.revokeSessionById(session.id, options);
|
||||
}
|
||||
|
||||
private buildSessionModel(
|
||||
options: CreateSessionOptions,
|
||||
tokenMeta: { refreshToken: string; refreshTokenExpiresAt: Date },
|
||||
): Model {
|
||||
const session: Model = new Model();
|
||||
session.projectId = options.projectId;
|
||||
session.statusPageId = options.statusPageId;
|
||||
session.statusPagePrivateUserId = options.statusPagePrivateUserId;
|
||||
session.refreshToken = HashedString.fromString(tokenMeta.refreshToken);
|
||||
session.refreshTokenExpiresAt = tokenMeta.refreshTokenExpiresAt;
|
||||
session.lastActiveAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if (options.userAgent) {
|
||||
session.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
const deviceName: string | undefined = Text.truncate(
|
||||
options.deviceName,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceName) {
|
||||
session.deviceName = deviceName;
|
||||
}
|
||||
|
||||
const deviceType: string | undefined = Text.truncate(
|
||||
options.deviceType,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceType) {
|
||||
session.deviceType = deviceType;
|
||||
}
|
||||
|
||||
const deviceOS: string | undefined = Text.truncate(
|
||||
options.deviceOS,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceOS) {
|
||||
session.deviceOS = deviceOS;
|
||||
}
|
||||
|
||||
const deviceBrowser: string | undefined = Text.truncate(
|
||||
options.deviceBrowser,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceBrowser) {
|
||||
session.deviceBrowser = deviceBrowser;
|
||||
}
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (ipAddress) {
|
||||
session.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
session.additionalInfo = {
|
||||
...(options.additionalInfo || {}),
|
||||
} as JSONObject;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private static generateRefreshToken(): string {
|
||||
return ObjectID.generate().toString();
|
||||
}
|
||||
|
||||
private static getRefreshTokenExpiry(): Date {
|
||||
return OneUptimeDate.getSomeDaysAfter(
|
||||
Service.DEFAULT_REFRESH_TOKEN_TTL_DAYS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -345,7 +345,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
},
|
||||
});
|
||||
|
||||
if (membersInTeam.toNumber() <= 1) {
|
||||
// Skip the one-member guard when SCIM manages membership for the project.
|
||||
const isSCIMEnabled: boolean = await this.isSCIMEnabled(
|
||||
member.projectId!,
|
||||
);
|
||||
|
||||
if (!isSCIMEnabled && membersInTeam.toNumber() <= 1) {
|
||||
throw new BadDataException(
|
||||
Errors.TeamMemberService.ONE_MEMBER_REQUIRED,
|
||||
);
|
||||
|
||||
350
Common/Server/Services/UserSessionService.ts
Normal file
350
Common/Server/Services/UserSessionService.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/UserSession";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import { EncryptionSecret } from "../EnvironmentConfig";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import Text from "../../Types/Text";
|
||||
import logger from "../Utils/Logger";
|
||||
import Exception from "../../Types/Exception/Exception";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
|
||||
export interface SessionMetadata {
|
||||
session: Model;
|
||||
refreshToken: string;
|
||||
refreshTokenExpiresAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateSessionOptions {
|
||||
userId: ObjectID;
|
||||
isGlobalLogin: boolean;
|
||||
refreshToken?: string | undefined;
|
||||
refreshTokenExpiresAt?: Date | undefined;
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
deviceName?: string | undefined;
|
||||
deviceType?: string | undefined;
|
||||
deviceOS?: string | undefined;
|
||||
deviceBrowser?: string | undefined;
|
||||
additionalInfo?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
export interface RenewSessionOptions {
|
||||
session: Model;
|
||||
refreshTokenExpiresAt?: Date | undefined;
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
deviceName?: string | undefined;
|
||||
deviceType?: string | undefined;
|
||||
deviceOS?: string | undefined;
|
||||
deviceBrowser?: string | undefined;
|
||||
additionalInfo?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
export interface TouchSessionOptions {
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
}
|
||||
|
||||
export interface RevokeSessionOptions {
|
||||
reason?: string | undefined;
|
||||
}
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
private static readonly DEFAULT_REFRESH_TOKEN_TTL_DAYS: number = 30;
|
||||
private static readonly SHORT_TEXT_LIMIT: number = 100;
|
||||
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
public async createSession(
|
||||
options: CreateSessionOptions,
|
||||
): Promise<SessionMetadata> {
|
||||
const refreshToken: string =
|
||||
options.refreshToken || Service.generateRefreshToken();
|
||||
const refreshTokenExpiresAt: Date =
|
||||
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
||||
|
||||
const session: Model = this.buildSessionModel(options, {
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
});
|
||||
|
||||
try {
|
||||
const createdSession: Model = await this.create({
|
||||
data: session,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
session: createdSession,
|
||||
refreshToken: refreshToken,
|
||||
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
||||
};
|
||||
} catch (error) {
|
||||
throw error as Exception;
|
||||
}
|
||||
}
|
||||
|
||||
public async findActiveSessionByRefreshToken(
|
||||
refreshToken: string,
|
||||
): Promise<Model | null> {
|
||||
const hashedValue: string = await HashedString.hashValue(
|
||||
refreshToken,
|
||||
EncryptionSecret,
|
||||
);
|
||||
|
||||
return await this.findOneBy({
|
||||
query: {
|
||||
refreshToken: new HashedString(hashedValue, true),
|
||||
isRevoked: false,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
refreshTokenExpiresAt: true,
|
||||
lastActiveAt: true,
|
||||
isRevoked: true,
|
||||
additionalInfo: true,
|
||||
deviceName: true,
|
||||
deviceType: true,
|
||||
deviceOS: true,
|
||||
deviceBrowser: true,
|
||||
ipAddress: true,
|
||||
userAgent: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async renewSessionWithNewRefreshToken(
|
||||
options: RenewSessionOptions,
|
||||
): Promise<SessionMetadata> {
|
||||
const refreshToken: string = Service.generateRefreshToken();
|
||||
const refreshTokenExpiresAt: Date =
|
||||
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
||||
|
||||
const updatePayload: Partial<Model> = {
|
||||
refreshToken: HashedString.fromString(refreshToken),
|
||||
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
||||
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
||||
isRevoked: false,
|
||||
};
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
|
||||
if (ipAddress) {
|
||||
updatePayload.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
if (options.userAgent) {
|
||||
updatePayload.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
const deviceName: string | undefined = Text.truncate(
|
||||
options.deviceName,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceName) {
|
||||
updatePayload.deviceName = deviceName;
|
||||
}
|
||||
|
||||
const deviceType: string | undefined = Text.truncate(
|
||||
options.deviceType,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceType) {
|
||||
updatePayload.deviceType = deviceType;
|
||||
}
|
||||
|
||||
const deviceOS: string | undefined = Text.truncate(
|
||||
options.deviceOS,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceOS) {
|
||||
updatePayload.deviceOS = deviceOS;
|
||||
}
|
||||
|
||||
const deviceBrowser: string | undefined = Text.truncate(
|
||||
options.deviceBrowser,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceBrowser) {
|
||||
updatePayload.deviceBrowser = deviceBrowser;
|
||||
}
|
||||
|
||||
if (options.additionalInfo || options.session.additionalInfo) {
|
||||
updatePayload.additionalInfo = {
|
||||
...(options.session.additionalInfo || {}),
|
||||
...(options.additionalInfo || {}),
|
||||
} as JSONObject;
|
||||
}
|
||||
|
||||
const updatedSession: Model | null = await this.updateOneByIdAndFetch({
|
||||
id: options.session.id!,
|
||||
data: updatePayload as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updatedSession) {
|
||||
throw new BadDataException("Unable to renew user session");
|
||||
}
|
||||
|
||||
return {
|
||||
session: updatedSession,
|
||||
refreshToken: refreshToken,
|
||||
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
public async touchSession(
|
||||
sessionId: ObjectID,
|
||||
options: TouchSessionOptions,
|
||||
): Promise<void> {
|
||||
const updatePayload: Partial<Model> = {
|
||||
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
||||
};
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
|
||||
if (ipAddress) {
|
||||
updatePayload.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
if (options.userAgent) {
|
||||
updatePayload.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.updateOneById({
|
||||
id: sessionId,
|
||||
data: updatePayload as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Failed to update session activity timestamp for session ${sessionId.toString()}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async revokeSessionById(
|
||||
sessionId: ObjectID,
|
||||
options?: RevokeSessionOptions,
|
||||
): Promise<void> {
|
||||
await this.updateOneById({
|
||||
id: sessionId,
|
||||
data: {
|
||||
isRevoked: true,
|
||||
revokedAt: OneUptimeDate.getCurrentDate(),
|
||||
revokedReason: options?.reason ?? null,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async revokeSessionByRefreshToken(
|
||||
refreshToken: string,
|
||||
options?: RevokeSessionOptions,
|
||||
): Promise<void> {
|
||||
const session: Model | null =
|
||||
await this.findActiveSessionByRefreshToken(refreshToken);
|
||||
|
||||
if (!session || !session.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.revokeSessionById(session.id, options);
|
||||
}
|
||||
|
||||
private buildSessionModel(
|
||||
options: CreateSessionOptions,
|
||||
tokenMeta: { refreshToken: string; refreshTokenExpiresAt: Date },
|
||||
): Model {
|
||||
const session: Model = new Model();
|
||||
session.userId = options.userId;
|
||||
session.refreshToken = HashedString.fromString(tokenMeta.refreshToken);
|
||||
session.refreshTokenExpiresAt = tokenMeta.refreshTokenExpiresAt;
|
||||
session.lastActiveAt = OneUptimeDate.getCurrentDate();
|
||||
if (options.userAgent) {
|
||||
session.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
const deviceName: string | undefined = Text.truncate(
|
||||
options.deviceName,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceName) {
|
||||
session.deviceName = deviceName;
|
||||
}
|
||||
|
||||
const deviceType: string | undefined = Text.truncate(
|
||||
options.deviceType,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceType) {
|
||||
session.deviceType = deviceType;
|
||||
}
|
||||
|
||||
const deviceOS: string | undefined = Text.truncate(
|
||||
options.deviceOS,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceOS) {
|
||||
session.deviceOS = deviceOS;
|
||||
}
|
||||
|
||||
const deviceBrowser: string | undefined = Text.truncate(
|
||||
options.deviceBrowser,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceBrowser) {
|
||||
session.deviceBrowser = deviceBrowser;
|
||||
}
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (ipAddress) {
|
||||
session.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
session.additionalInfo = {
|
||||
...(options.additionalInfo || {}),
|
||||
isGlobalLogin: options.isGlobalLogin,
|
||||
} as JSONObject;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private static generateRefreshToken(): string {
|
||||
return ObjectID.generate().toString();
|
||||
}
|
||||
|
||||
private static getRefreshTokenExpiry(): Date {
|
||||
return OneUptimeDate.getSomeDaysAfter(
|
||||
Service.DEFAULT_REFRESH_TOKEN_TTL_DAYS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -4,6 +4,7 @@ import ObjectID from "../../Types/ObjectID";
|
||||
import { CookieOptions } from "express";
|
||||
import JSONWebToken from "./JsonWebToken";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import StatusPagePrivateUser from "../../Models/DatabaseModels/StatusPagePrivateUser";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import PositiveNumber from "../../Types/PositiveNumber";
|
||||
import CookieName from "../../Types/CookieName";
|
||||
@@ -12,6 +13,8 @@ import CaptureSpan from "./Telemetry/CaptureSpan";
|
||||
export default class CookieUtil {
|
||||
// set cookie with express response
|
||||
|
||||
private static readonly DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
@CaptureSpan()
|
||||
public static getCookiesFromCookieString(
|
||||
cookieString: string,
|
||||
@@ -58,8 +61,23 @@ export default class CookieUtil {
|
||||
expressResponse: ExpressResponse;
|
||||
user: User;
|
||||
isGlobalLogin: boolean;
|
||||
sessionId: ObjectID;
|
||||
refreshToken: string;
|
||||
refreshTokenExpiresAt: Date;
|
||||
accessTokenExpiresInSeconds?: number;
|
||||
}): void {
|
||||
const { expressResponse: res, user, isGlobalLogin } = data;
|
||||
const {
|
||||
expressResponse: res,
|
||||
user,
|
||||
isGlobalLogin,
|
||||
sessionId,
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
} = data;
|
||||
|
||||
const accessTokenExpiresInSeconds: number =
|
||||
data.accessTokenExpiresInSeconds ||
|
||||
CookieUtil.DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS;
|
||||
|
||||
const token: string = JSONWebToken.signUserLoginToken({
|
||||
tokenData: {
|
||||
@@ -69,13 +87,24 @@ export default class CookieUtil {
|
||||
timezone: user.timezone || null,
|
||||
isMasterAdmin: user.isMasterAdmin!,
|
||||
isGlobalLogin: isGlobalLogin, // This is a general login without SSO. So, we will set this to true. This will give access to all the projects that dont require SSO.
|
||||
sessionId: sessionId,
|
||||
},
|
||||
expiresInSeconds: OneUptimeDate.getSecondsInDays(new PositiveNumber(30)),
|
||||
expiresInSeconds: accessTokenExpiresInSeconds,
|
||||
});
|
||||
|
||||
// Set a cookie with token.
|
||||
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), token, {
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
maxAge: accessTokenExpiresInSeconds * 1000,
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
const refreshTokenTtl: number = Math.max(
|
||||
refreshTokenExpiresAt.getTime() - Date.now(),
|
||||
0,
|
||||
);
|
||||
|
||||
CookieUtil.setCookie(res, CookieUtil.getRefreshTokenKey(), refreshToken, {
|
||||
maxAge: refreshTokenTtl,
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
@@ -148,6 +177,62 @@ export default class CookieUtil {
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static setStatusPagePrivateUserCookie(data: {
|
||||
expressResponse: ExpressResponse;
|
||||
user: StatusPagePrivateUser;
|
||||
statusPageId: ObjectID;
|
||||
sessionId: ObjectID;
|
||||
refreshToken: string;
|
||||
refreshTokenExpiresAt: Date;
|
||||
accessTokenExpiresInSeconds?: number;
|
||||
}): string {
|
||||
const {
|
||||
expressResponse: res,
|
||||
user,
|
||||
statusPageId,
|
||||
sessionId,
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
} = data;
|
||||
|
||||
const accessTokenExpiresInSeconds: number =
|
||||
data.accessTokenExpiresInSeconds ||
|
||||
CookieUtil.DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS;
|
||||
|
||||
const token: string = JSONWebToken.sign({
|
||||
data: {
|
||||
userId: user.id!,
|
||||
email: user.email!,
|
||||
statusPageId: statusPageId,
|
||||
sessionId: sessionId,
|
||||
},
|
||||
expiresInSeconds: accessTokenExpiresInSeconds,
|
||||
});
|
||||
|
||||
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(statusPageId), token, {
|
||||
maxAge: accessTokenExpiresInSeconds * 1000,
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
const refreshTokenTtl: number = Math.max(
|
||||
refreshTokenExpiresAt.getTime() - Date.now(),
|
||||
0,
|
||||
);
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
refreshToken,
|
||||
{
|
||||
maxAge: refreshTokenTtl,
|
||||
httpOnly: true,
|
||||
},
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static setCookie(
|
||||
res: ExpressResponse,
|
||||
@@ -155,7 +240,13 @@ export default class CookieUtil {
|
||||
value: string,
|
||||
options: CookieOptions,
|
||||
): void {
|
||||
res.cookie(name, value, options);
|
||||
const cookieOptions: CookieOptions = {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
...options,
|
||||
};
|
||||
|
||||
res.cookie(name, value, cookieOptions);
|
||||
}
|
||||
|
||||
// get cookie with express request
|
||||
@@ -168,11 +259,25 @@ export default class CookieUtil {
|
||||
return req.cookies[name];
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getRefreshTokenFromExpressRequest(
|
||||
req: ExpressRequest,
|
||||
id?: ObjectID,
|
||||
): string | undefined {
|
||||
return CookieUtil.getCookieFromExpressRequest(
|
||||
req,
|
||||
CookieUtil.getRefreshTokenKey(id),
|
||||
);
|
||||
}
|
||||
|
||||
// delete cookie with express response
|
||||
|
||||
@CaptureSpan()
|
||||
public static removeCookie(res: ExpressResponse, name: string): void {
|
||||
res.clearCookie(name);
|
||||
res.clearCookie(name, {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
});
|
||||
}
|
||||
|
||||
// get all cookies with express request
|
||||
@@ -190,6 +295,15 @@ export default class CookieUtil {
|
||||
return `${CookieName.Token}-${id.toString()}`;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getRefreshTokenKey(id?: ObjectID): string {
|
||||
if (!id) {
|
||||
return CookieName.RefreshToken;
|
||||
}
|
||||
|
||||
return `${CookieName.RefreshToken}-${id.toString()}`;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getUserSSOKey(id: ObjectID): string {
|
||||
return `${this.getSSOKey()}${id.toString()}`;
|
||||
@@ -210,5 +324,8 @@ export default class CookieUtil {
|
||||
for (const key in cookies) {
|
||||
this.removeCookie(res, key);
|
||||
}
|
||||
|
||||
// Always attempt to remove refresh token cookie even if not parsed.
|
||||
this.removeCookie(res, this.getRefreshTokenKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,15 @@ export interface OneUptimeResponse extends express.Response {
|
||||
logBody: JSONObjectOrArray;
|
||||
}
|
||||
|
||||
export type RequestDeviceInfo = {
|
||||
deviceName?: string;
|
||||
deviceType?: string;
|
||||
deviceOS?: string;
|
||||
deviceBrowser?: string;
|
||||
};
|
||||
|
||||
type HeaderValue = string | Array<string> | null | undefined;
|
||||
|
||||
class Express {
|
||||
private static app: express.Application;
|
||||
private static httpServer: Server;
|
||||
@@ -101,3 +110,94 @@ class Express {
|
||||
}
|
||||
|
||||
export default Express;
|
||||
|
||||
export const headerValueToString: (value: HeaderValue) => string | undefined = (
|
||||
value: HeaderValue,
|
||||
): string | undefined => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0 ? value[0] : undefined;
|
||||
}
|
||||
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const extractDeviceInfo: (req: ExpressRequest) => RequestDeviceInfo = (
|
||||
req: ExpressRequest,
|
||||
): RequestDeviceInfo => {
|
||||
const body: JSONObject = (req.body || {}) as JSONObject;
|
||||
const data: JSONObject = (body["data"] as JSONObject) || {};
|
||||
|
||||
const getValue: (key: string) => string | undefined = (
|
||||
key: string,
|
||||
): string | undefined => {
|
||||
const headerKey: string = key.toLowerCase();
|
||||
const camelKey: string = headerKey
|
||||
.split("-")
|
||||
.map((part: string, index: number) => {
|
||||
if (index === 0) {
|
||||
return part;
|
||||
}
|
||||
|
||||
return part.charAt(0).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join("");
|
||||
|
||||
return (
|
||||
headerValueToString(req.headers[`x-${headerKey}`]) ||
|
||||
headerValueToString(body[camelKey] as HeaderValue) ||
|
||||
headerValueToString(data[camelKey] as HeaderValue) ||
|
||||
headerValueToString(body[key] as HeaderValue) ||
|
||||
headerValueToString(data[key] as HeaderValue)
|
||||
);
|
||||
};
|
||||
|
||||
const result: RequestDeviceInfo = {};
|
||||
|
||||
const deviceName: string | undefined = getValue("device-name");
|
||||
if (deviceName) {
|
||||
result.deviceName = deviceName;
|
||||
}
|
||||
|
||||
const deviceType: string | undefined = getValue("device-type");
|
||||
if (deviceType) {
|
||||
result.deviceType = deviceType;
|
||||
}
|
||||
|
||||
const deviceOS: string | undefined = getValue("device-os");
|
||||
if (deviceOS) {
|
||||
result.deviceOS = deviceOS;
|
||||
}
|
||||
|
||||
const deviceBrowser: string | undefined = getValue("device-browser");
|
||||
if (deviceBrowser) {
|
||||
result.deviceBrowser = deviceBrowser;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getClientIp: (req: ExpressRequest) => string | undefined = (
|
||||
req: ExpressRequest,
|
||||
): string | undefined => {
|
||||
const forwarded: string | Array<string> | undefined = req.headers[
|
||||
"x-forwarded-for"
|
||||
] as string | Array<string> | undefined;
|
||||
|
||||
if (Array.isArray(forwarded) && forwarded.length > 0) {
|
||||
return forwarded[0]?.split(",")[0]?.trim();
|
||||
}
|
||||
|
||||
if (typeof forwarded === "string" && forwarded.trim().length > 0) {
|
||||
return forwarded.split(",")[0]?.trim();
|
||||
}
|
||||
|
||||
if (req.socket?.remoteAddress) {
|
||||
return req.socket.remoteAddress;
|
||||
}
|
||||
|
||||
return req.ip;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ class JSONWebToken {
|
||||
isMasterAdmin: boolean;
|
||||
// If this is OneUptime username and password login. This is true, if this is SSO login. Then, this is false.
|
||||
isGlobalLogin: boolean;
|
||||
sessionId: ObjectID;
|
||||
};
|
||||
expiresInSeconds: number;
|
||||
}): string {
|
||||
@@ -67,9 +68,9 @@ class JSONWebToken {
|
||||
name: data.name?.toString() || "",
|
||||
projectId: data.projectId?.toString() || "",
|
||||
isMasterAdmin: data.isMasterAdmin,
|
||||
sessionId: data.sessionId?.toString() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return JSONWebToken.signJsonPayload(jsonObj, expiresInSeconds);
|
||||
}
|
||||
|
||||
@@ -106,6 +107,9 @@ class JSONWebToken {
|
||||
isMasterAdmin: false,
|
||||
name: new Name("User"),
|
||||
isGlobalLogin: Boolean(decoded["isGlobalLogin"]),
|
||||
sessionId: decoded["sessionId"]
|
||||
? new ObjectID(decoded["sessionId"] as string)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +122,9 @@ class JSONWebToken {
|
||||
: undefined,
|
||||
isMasterAdmin: Boolean(decoded["isMasterAdmin"]),
|
||||
isGlobalLogin: Boolean(decoded["isGlobalLogin"]),
|
||||
sessionId: decoded["sessionId"]
|
||||
? new ObjectID(decoded["sessionId"] as string)
|
||||
: undefined,
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
@@ -636,7 +636,15 @@ export default class CompareCriteria {
|
||||
}
|
||||
|
||||
if (typeof value === Typeof.Number) {
|
||||
return (value as number).toFixed(2);
|
||||
const numericValue: number = value as number;
|
||||
|
||||
if (Number.isInteger(numericValue)) {
|
||||
return numericValue.toString();
|
||||
}
|
||||
|
||||
const roundedValue: number = Number(numericValue.toFixed(2));
|
||||
|
||||
return roundedValue.toString();
|
||||
}
|
||||
|
||||
if (typeof value === Typeof.Boolean) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import ExceptionMonitorResponse from "../../../../Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse";
|
||||
import CaptureSpan from "../../Telemetry/CaptureSpan";
|
||||
import DataToProcess from "../DataToProcess";
|
||||
import CompareCriteria from "./CompareCriteria";
|
||||
import {
|
||||
CheckOn,
|
||||
CriteriaFilter,
|
||||
} from "../../../../Types/Monitor/CriteriaFilter";
|
||||
|
||||
export default class ExceptionMonitorCriteria {
|
||||
@CaptureSpan()
|
||||
public static async isMonitorInstanceCriteriaFilterMet(input: {
|
||||
dataToProcess: DataToProcess;
|
||||
criteriaFilter: CriteriaFilter;
|
||||
}): Promise<string | null> {
|
||||
let threshold: number | string | undefined | null =
|
||||
input.criteriaFilter.value;
|
||||
|
||||
if (input.criteriaFilter.checkOn === CheckOn.ExceptionCount) {
|
||||
threshold = CompareCriteria.convertToNumber(threshold);
|
||||
|
||||
const currentExceptionCount: number =
|
||||
(input.dataToProcess as ExceptionMonitorResponse).exceptionCount || 0;
|
||||
|
||||
return CompareCriteria.compareCriteriaNumbers({
|
||||
value: currentExceptionCount,
|
||||
threshold: threshold as number,
|
||||
criteriaFilter: input.criteriaFilter,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
|
||||
import LogMonitorResponse from "../../../Types/Monitor/LogMonitor/LogMonitorResponse";
|
||||
import TraceMonitorResponse from "../../../Types/Monitor/TraceMonitor/TraceMonitorResponse";
|
||||
import MetricMonitorResponse from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
||||
import ExceptionMonitorResponse from "../../../Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse";
|
||||
|
||||
type DataToProcess =
|
||||
| ProbeMonitorResponse
|
||||
@@ -11,6 +12,7 @@ type DataToProcess =
|
||||
| ServerMonitorResponse
|
||||
| LogMonitorResponse
|
||||
| TraceMonitorResponse
|
||||
| MetricMonitorResponse;
|
||||
| MetricMonitorResponse
|
||||
| ExceptionMonitorResponse;
|
||||
|
||||
export default DataToProcess;
|
||||
|
||||
@@ -14,6 +14,7 @@ import AggregatedResult from "../../../Types/BaseDatabase/AggregatedResult";
|
||||
import AggregateModel from "../../../Types/BaseDatabase/AggregatedModel";
|
||||
import MetricQueryConfigData from "../../../Types/Metrics/MetricQueryConfigData";
|
||||
import MetricFormulaConfigData from "../../../Types/Metrics/MetricFormulaConfigData";
|
||||
import ExceptionMonitorResponse from "../../../Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse";
|
||||
|
||||
export default class MonitorCriteriaDataExtractor {
|
||||
public static getProbeMonitorResponse(
|
||||
@@ -79,6 +80,18 @@ export default class MonitorCriteriaDataExtractor {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static getExceptionMonitorResponse(
|
||||
dataToProcess: DataToProcess,
|
||||
): ExceptionMonitorResponse | null {
|
||||
if (
|
||||
(dataToProcess as ExceptionMonitorResponse).exceptionCount !== undefined
|
||||
) {
|
||||
return dataToProcess as ExceptionMonitorResponse;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static getCustomCodeMonitorResponse(
|
||||
dataToProcess: DataToProcess,
|
||||
): CustomCodeMonitorResponse | null {
|
||||
|
||||
@@ -9,6 +9,7 @@ import SyntheticMonitoringCriteria from "./Criteria/SyntheticMonitor";
|
||||
import LogMonitorCriteria from "./Criteria/LogMonitorCriteria";
|
||||
import MetricMonitorCriteria from "./Criteria/MetricMonitorCriteria";
|
||||
import TraceMonitorCriteria from "./Criteria/TraceMonitorCriteria";
|
||||
import ExceptionMonitorCriteria from "./Criteria/ExceptionMonitorCriteria";
|
||||
import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
|
||||
import DataToProcess from "./DataToProcess";
|
||||
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
||||
@@ -434,6 +435,18 @@ export default class MonitorCriteriaEvaluator {
|
||||
}
|
||||
}
|
||||
|
||||
if (input.monitor.monitorType === MonitorType.Exceptions) {
|
||||
const exceptionMonitorResult: string | null =
|
||||
await ExceptionMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
||||
dataToProcess: input.dataToProcess,
|
||||
criteriaFilter: input.criteriaFilter,
|
||||
});
|
||||
|
||||
if (exceptionMonitorResult) {
|
||||
return exceptionMonitorResult;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import SyntheticMonitorResponse from "../../../Types/Monitor/SyntheticMonitors/S
|
||||
import CustomCodeMonitorResponse from "../../../Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse";
|
||||
import LogMonitorResponse from "../../../Types/Monitor/LogMonitor/LogMonitorResponse";
|
||||
import TraceMonitorResponse from "../../../Types/Monitor/TraceMonitor/TraceMonitorResponse";
|
||||
import ExceptionMonitorResponse from "../../../Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse";
|
||||
import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
|
||||
import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
|
||||
import MonitorCriteriaExpectationBuilder from "./MonitorCriteriaExpectationBuilder";
|
||||
@@ -161,6 +162,10 @@ export default class MonitorCriteriaObservationBuilder {
|
||||
return MonitorCriteriaObservationBuilder.describeMetricValueObservation(
|
||||
input,
|
||||
);
|
||||
case CheckOn.ExceptionCount:
|
||||
return MonitorCriteriaObservationBuilder.describeExceptionCountObservation(
|
||||
input,
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -1083,6 +1088,21 @@ export default class MonitorCriteriaObservationBuilder {
|
||||
return `Span count was ${traceResponse.spanCount}.`;
|
||||
}
|
||||
|
||||
private static describeExceptionCountObservation(input: {
|
||||
dataToProcess: DataToProcess;
|
||||
}): string | null {
|
||||
const exceptionResponse: ExceptionMonitorResponse | null =
|
||||
MonitorCriteriaDataExtractor.getExceptionMonitorResponse(
|
||||
input.dataToProcess,
|
||||
);
|
||||
|
||||
if (!exceptionResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `Exception count was ${exceptionResponse.exceptionCount}.`;
|
||||
}
|
||||
|
||||
private static describeMetricValueObservation(input: {
|
||||
criteriaFilter: CriteriaFilter;
|
||||
dataToProcess: DataToProcess;
|
||||
|
||||
@@ -31,6 +31,7 @@ import LogMonitorResponse from "../../../Types/Monitor/LogMonitor/LogMonitorResp
|
||||
import MetricMonitorResponse from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
||||
import TelemetryType from "../../../Types/Telemetry/TelemetryType";
|
||||
import TraceMonitorResponse from "../../../Types/Monitor/TraceMonitor/TraceMonitorResponse";
|
||||
import ExceptionMonitorResponse from "../../../Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse";
|
||||
import { TelemetryQuery } from "../../../Types/Telemetry/TelemetryQuery";
|
||||
import MonitorIncident from "./MonitorIncident";
|
||||
import MonitorAlert from "./MonitorAlert";
|
||||
@@ -557,6 +558,23 @@ export default class MonitorResourceUtil {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
dataToProcess &&
|
||||
(dataToProcess as ExceptionMonitorResponse).exceptionQuery
|
||||
) {
|
||||
const exceptionResponse: ExceptionMonitorResponse =
|
||||
dataToProcess as ExceptionMonitorResponse;
|
||||
telemetryQuery = {
|
||||
telemetryQuery: exceptionResponse.exceptionQuery,
|
||||
telemetryType: TelemetryType.Exception,
|
||||
metricViewData: null,
|
||||
};
|
||||
|
||||
logger.debug(
|
||||
`${dataToProcess.monitorId.toString()} - Exception query found.`,
|
||||
);
|
||||
}
|
||||
|
||||
const matchedCriteriaInstance: MonitorCriteriaInstance =
|
||||
criteriaInstanceMap[response.criteriaMetId!]!;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import OpenTelemetryAPI, {
|
||||
* DiagLogLevel,
|
||||
*/
|
||||
Meter,
|
||||
type AttributeValue,
|
||||
} from "@opentelemetry/api";
|
||||
import { Logger, logs } from "@opentelemetry/api-logs";
|
||||
import {
|
||||
@@ -22,19 +23,27 @@ import {
|
||||
BatchLogRecordProcessor,
|
||||
LoggerProvider,
|
||||
LogRecordProcessor,
|
||||
type LoggerProviderConfig,
|
||||
} from "@opentelemetry/sdk-logs";
|
||||
import type { Resource as LogsResource } from "@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources/build/src/Resource";
|
||||
import {
|
||||
Aggregation,
|
||||
MeterProvider,
|
||||
PeriodicExportingMetricReader,
|
||||
} from "@opentelemetry/sdk-metrics";
|
||||
import type { PushMetricExporter } from "@opentelemetry/sdk-metrics/build/src/export/MetricExporter";
|
||||
import * as opentelemetry from "@opentelemetry/sdk-node";
|
||||
import { SpanExporter } from "@opentelemetry/sdk-trace-node";
|
||||
import { SpanExporter } from "@opentelemetry/sdk-trace-base";
|
||||
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
||||
import URL from "../../Types/API/URL";
|
||||
import Dictionary from "../../Types/Dictionary";
|
||||
import { DisableTelemetry } from "../EnvironmentConfig";
|
||||
import logger from "./Logger";
|
||||
|
||||
type ResourceWithRawAttributes = LogsResource & {
|
||||
getRawAttributes?: () => Array<[string, AttributeValue | undefined]>;
|
||||
};
|
||||
|
||||
/*
|
||||
* Enable this line to see debug logs
|
||||
* diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
|
||||
@@ -156,26 +165,51 @@ export default class Telemetry {
|
||||
url: this.getOltpTracesEndpoint()!.toString(),
|
||||
headers: headers,
|
||||
compression: CompressionAlgorithm.GZIP,
|
||||
});
|
||||
}) as unknown as SpanExporter;
|
||||
}
|
||||
|
||||
if (this.getOltpMetricsEndpoint() && hasHeaders) {
|
||||
const metricExporter: PushMetricExporter = new OTLPMetricExporter({
|
||||
url: this.getOltpMetricsEndpoint()!.toString(),
|
||||
headers: headers,
|
||||
compression: CompressionAlgorithm.GZIP,
|
||||
}) as unknown as PushMetricExporter;
|
||||
|
||||
// Force an SDK-side aggregation selector that matches the modern metrics API.
|
||||
if (
|
||||
typeof (metricExporter as { selectAggregation?: unknown })
|
||||
.selectAggregation === "function"
|
||||
) {
|
||||
(
|
||||
metricExporter as unknown as {
|
||||
selectAggregation: (..._args: Array<unknown>) => Aggregation;
|
||||
}
|
||||
).selectAggregation = () => {
|
||||
return Aggregation.Default();
|
||||
};
|
||||
}
|
||||
|
||||
this.metricReader = new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({
|
||||
url: this.getOltpMetricsEndpoint()!.toString(),
|
||||
headers: headers,
|
||||
compression: CompressionAlgorithm.GZIP,
|
||||
}),
|
||||
exporter: metricExporter,
|
||||
});
|
||||
}
|
||||
|
||||
this.loggerProvider = new LoggerProvider({
|
||||
resource: this.getResource({
|
||||
serviceName: data.serviceName,
|
||||
}),
|
||||
const resource: Resource = this.getResource({
|
||||
serviceName: data.serviceName,
|
||||
});
|
||||
|
||||
let logRecordProcessor: LogRecordProcessor | null = null;
|
||||
const logRecordProcessors: Array<LogRecordProcessor> = [];
|
||||
|
||||
const loggerProviderResource: ResourceWithRawAttributes =
|
||||
resource as unknown as ResourceWithRawAttributes;
|
||||
|
||||
if (typeof loggerProviderResource.getRawAttributes !== "function") {
|
||||
loggerProviderResource.getRawAttributes = () => {
|
||||
return Object.entries(resource.attributes) as Array<
|
||||
[string, AttributeValue | undefined]
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
if (this.getOltpLogsEndpoint() && hasHeaders) {
|
||||
const logExporter: OTLPLogExporter = new OTLPLogExporter({
|
||||
@@ -184,22 +218,27 @@ export default class Telemetry {
|
||||
compression: CompressionAlgorithm.GZIP,
|
||||
});
|
||||
|
||||
logRecordProcessor = new BatchLogRecordProcessor(logExporter);
|
||||
logRecordProcessors.push(new BatchLogRecordProcessor(logExporter));
|
||||
}
|
||||
|
||||
if (logRecordProcessor) {
|
||||
this.loggerProvider.addLogRecordProcessor(logRecordProcessor);
|
||||
const loggerProviderConfig: LoggerProviderConfig = {
|
||||
resource: loggerProviderResource,
|
||||
};
|
||||
|
||||
if (logRecordProcessors.length > 0) {
|
||||
loggerProviderConfig.processors = logRecordProcessors;
|
||||
}
|
||||
|
||||
this.loggerProvider = new LoggerProvider(loggerProviderConfig);
|
||||
|
||||
logs.setGlobalLoggerProvider(this.loggerProvider);
|
||||
|
||||
const nodeSdkConfiguration: Partial<opentelemetry.NodeSDKConfiguration> =
|
||||
{
|
||||
idGenerator: new AWSXRayIdGenerator(),
|
||||
instrumentations: [],
|
||||
resource: this.getResource({
|
||||
serviceName: data.serviceName,
|
||||
}),
|
||||
resource:
|
||||
loggerProviderResource as unknown as opentelemetry.NodeSDKConfiguration["resource"],
|
||||
autoDetectResources: true,
|
||||
};
|
||||
|
||||
@@ -214,8 +253,8 @@ export default class Telemetry {
|
||||
* }
|
||||
*/
|
||||
|
||||
if (logRecordProcessor) {
|
||||
nodeSdkConfiguration.logRecordProcessor = logRecordProcessor;
|
||||
if (logRecordProcessors.length > 0) {
|
||||
nodeSdkConfiguration.logRecordProcessors = logRecordProcessors;
|
||||
}
|
||||
|
||||
const sdk: opentelemetry.NodeSDK = new opentelemetry.NodeSDK(
|
||||
|
||||
@@ -2,6 +2,7 @@ import Pill, { PillSize } from "../../../UI/Components/Pill/Pill";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import Color from "../../../Types/Color";
|
||||
import IconProp from "../../../Types/Icon/IconProp";
|
||||
import * as React from "react";
|
||||
import { describe, expect, test } from "@jest/globals";
|
||||
|
||||
@@ -46,4 +47,11 @@ describe("<Pill />", () => {
|
||||
render(<Pill text="Love" color={color} size={PillSize.ExtraLarge} />);
|
||||
expect(screen.getByTestId("pill")).toHaveStyle("backgroundColor: #786598");
|
||||
});
|
||||
test("renders icon when provided", () => {
|
||||
const color: Color = new Color("#807149");
|
||||
const { container } = render(
|
||||
<Pill text="Love" color={color} icon={IconProp.Label} />,
|
||||
);
|
||||
expect(container.querySelector('[role="icon"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,9 +19,9 @@ export default class Color extends DatabaseProperty {
|
||||
this._color = v;
|
||||
}
|
||||
|
||||
public constructor(color: string) {
|
||||
public constructor(color: string | Color) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.color = color.toString();
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
|
||||
@@ -2,6 +2,7 @@ enum CookieName {
|
||||
UserID = "user-id",
|
||||
Email = "user-email",
|
||||
Token = "user-token",
|
||||
RefreshToken = "user-refresh-token",
|
||||
Name = "user-name",
|
||||
Timezone = "user-timezone",
|
||||
IsMasterAdmin = "user-is-master-admin",
|
||||
|
||||
64
Common/Types/Database/ColorField.ts
Normal file
64
Common/Types/Database/ColorField.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import { ReflectionMetadataType } from "../Reflection";
|
||||
import "reflect-metadata";
|
||||
|
||||
const colorFieldSymbol: symbol = Symbol("ColorField");
|
||||
|
||||
type ColorFieldColumnsFunction = <T extends BaseModel>(
|
||||
target: T,
|
||||
) => Array<string>;
|
||||
|
||||
type FirstColorFieldColumnFunction = <T extends BaseModel>(
|
||||
target: T,
|
||||
) => string | null;
|
||||
|
||||
type IsColorFieldColumnFunction = <T extends BaseModel>(
|
||||
target: T,
|
||||
propertyKey: string,
|
||||
) => boolean;
|
||||
|
||||
const ColorField: () => ReflectionMetadataType = () => {
|
||||
return Reflect.metadata(colorFieldSymbol, true);
|
||||
};
|
||||
|
||||
export const isColorFieldColumn: IsColorFieldColumnFunction = <
|
||||
T extends BaseModel,
|
||||
>(
|
||||
target: T,
|
||||
propertyKey: string,
|
||||
): boolean => {
|
||||
return Boolean(Reflect.getMetadata(colorFieldSymbol, target, propertyKey));
|
||||
};
|
||||
|
||||
export const getColorFieldColumns: ColorFieldColumnsFunction = <
|
||||
T extends BaseModel,
|
||||
>(
|
||||
target: T,
|
||||
): Array<string> => {
|
||||
const columns: Array<string> = [];
|
||||
const keys: Array<string> = Object.keys(target);
|
||||
|
||||
for (const key of keys) {
|
||||
if (isColorFieldColumn(target, key)) {
|
||||
columns.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getFirstColorFieldColumn: FirstColorFieldColumnFunction = <
|
||||
T extends BaseModel,
|
||||
>(
|
||||
target: T,
|
||||
): string | null => {
|
||||
const columns: Array<string> = getColorFieldColumns(target);
|
||||
|
||||
if (columns.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return columns[0] as string;
|
||||
};
|
||||
|
||||
export default ColorField;
|
||||
@@ -98,6 +98,7 @@ enum IconProp {
|
||||
False = "False",
|
||||
Text = "Text",
|
||||
Circle = "Circle",
|
||||
EmptyCircle = "EmptyCircle",
|
||||
Webhook = "Webhook",
|
||||
SendMessage = "SendMessage",
|
||||
ExternalLink = "ExternalLink",
|
||||
|
||||
@@ -11,4 +11,5 @@ export default interface JSONWebTokenData extends JSONObject {
|
||||
statusPageId?: ObjectID | undefined; // for status page logins.
|
||||
projectId?: ObjectID | undefined; // for SSO logins.
|
||||
isGlobalLogin: boolean; // If this is OneUptime username and password login. This is true, if this is SSO login. Then, this is false.
|
||||
sessionId?: ObjectID | undefined;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ export enum CheckOn {
|
||||
// Trace monitors.
|
||||
SpanCount = "Span Count",
|
||||
|
||||
// Exception monitors.
|
||||
ExceptionCount = "Exception Count",
|
||||
|
||||
// Metric Monitors.
|
||||
MetricValue = "Metric Value",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import Query from "../../BaseDatabase/Query";
|
||||
import ObjectID from "../../ObjectID";
|
||||
import ExceptionInstance from "../../../Models/AnalyticsModels/ExceptionInstance";
|
||||
import MonitorEvaluationSummary from "../MonitorEvaluationSummary";
|
||||
|
||||
export default interface ExceptionMonitorResponse {
|
||||
projectId: ObjectID;
|
||||
exceptionCount: number;
|
||||
exceptionQuery: Query<ExceptionInstance>;
|
||||
monitorId: ObjectID;
|
||||
evaluationSummary?: MonitorEvaluationSummary | undefined;
|
||||
}
|
||||
@@ -121,6 +121,33 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
return monitorCriteriaInstance;
|
||||
}
|
||||
|
||||
if (arg.monitorType === MonitorType.Exceptions) {
|
||||
const monitorCriteriaInstance: MonitorCriteriaInstance =
|
||||
new MonitorCriteriaInstance();
|
||||
|
||||
monitorCriteriaInstance.data = {
|
||||
id: ObjectID.generate().toString(),
|
||||
monitorStatusId: arg.monitorStatusId,
|
||||
filterCondition: FilterCondition.Any,
|
||||
filters: [
|
||||
{
|
||||
checkOn: CheckOn.ExceptionCount,
|
||||
filterType: FilterType.EqualTo,
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
incidents: [],
|
||||
alerts: [],
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: false,
|
||||
createAlerts: false,
|
||||
name: `Check if ${arg.monitorName} has no exceptions`,
|
||||
description: `This criteria checks if the ${arg.monitorName} has no exceptions.`,
|
||||
};
|
||||
|
||||
return monitorCriteriaInstance;
|
||||
}
|
||||
|
||||
if (arg.monitorType === MonitorType.Metrics) {
|
||||
const monitorCriteriaInstance: MonitorCriteriaInstance =
|
||||
new MonitorCriteriaInstance();
|
||||
@@ -462,6 +489,46 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
};
|
||||
}
|
||||
|
||||
if (arg.monitorType === MonitorType.Exceptions) {
|
||||
monitorCriteriaInstance.data = {
|
||||
id: ObjectID.generate().toString(),
|
||||
monitorStatusId: arg.monitorStatusId,
|
||||
filterCondition: FilterCondition.Any,
|
||||
filters: [
|
||||
{
|
||||
checkOn: CheckOn.ExceptionCount,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
incidents: [
|
||||
{
|
||||
title: `${arg.monitorName} has exceptions`,
|
||||
description: `${arg.monitorName} has active exceptions.`,
|
||||
incidentSeverityId: arg.incidentSeverityId,
|
||||
autoResolveIncident: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
onCallPolicyIds: [],
|
||||
},
|
||||
],
|
||||
alerts: [
|
||||
{
|
||||
title: `${arg.monitorName} has exceptions`,
|
||||
description: `${arg.monitorName} has active exceptions.`,
|
||||
alertSeverityId: arg.alertSeverityId,
|
||||
autoResolveAlert: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
onCallPolicyIds: [],
|
||||
},
|
||||
],
|
||||
createAlerts: false,
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: true,
|
||||
name: `Check if ${arg.monitorName} has exceptions`,
|
||||
description: `This criteria checks if the ${arg.monitorName} has exceptions.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (arg.monitorType === MonitorType.Metrics) {
|
||||
monitorCriteriaInstance.data = {
|
||||
id: ObjectID.generate().toString(),
|
||||
|
||||
@@ -23,6 +23,9 @@ import MonitorStepTraceMonitor, {
|
||||
import MonitorStepMetricMonitor, {
|
||||
MonitorStepMetricMonitorUtil,
|
||||
} from "./MonitorStepMetricMonitor";
|
||||
import MonitorStepExceptionMonitor, {
|
||||
MonitorStepExceptionMonitorUtil,
|
||||
} from "./MonitorStepExceptionMonitor";
|
||||
import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
|
||||
|
||||
export interface MonitorStepType {
|
||||
@@ -57,6 +60,9 @@ export interface MonitorStepType {
|
||||
|
||||
// Metric Monitor
|
||||
metricMonitor: MonitorStepMetricMonitor | undefined;
|
||||
|
||||
// Exception monitor
|
||||
exceptionMonitor?: MonitorStepExceptionMonitor | undefined;
|
||||
}
|
||||
|
||||
export default class MonitorStep extends DatabaseProperty {
|
||||
@@ -80,6 +86,7 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
logMonitor: undefined,
|
||||
traceMonitor: undefined,
|
||||
metricMonitor: undefined,
|
||||
exceptionMonitor: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,6 +115,7 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
logMonitor: undefined,
|
||||
traceMonitor: undefined,
|
||||
metricMonitor: undefined,
|
||||
exceptionMonitor: undefined,
|
||||
};
|
||||
|
||||
return monitorStep;
|
||||
@@ -186,6 +194,13 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
return this;
|
||||
}
|
||||
|
||||
public setExceptionMonitor(
|
||||
exceptionMonitor: MonitorStepExceptionMonitor,
|
||||
): MonitorStep {
|
||||
this.data!.exceptionMonitor = exceptionMonitor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setCustomCode(customCode: string): MonitorStep {
|
||||
this.data!.customCode = customCode;
|
||||
return this;
|
||||
@@ -212,6 +227,7 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
screenSizeTypes: undefined,
|
||||
browserTypes: undefined,
|
||||
lgoMonitor: undefined,
|
||||
exceptionMonitor: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -310,6 +326,12 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
MonitorStepTraceMonitorUtil.getDefault(),
|
||||
)
|
||||
: undefined,
|
||||
exceptionMonitor: this.data.exceptionMonitor
|
||||
? MonitorStepExceptionMonitorUtil.toJSON(
|
||||
this.data.exceptionMonitor ||
|
||||
MonitorStepExceptionMonitorUtil.getDefault(),
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -408,6 +430,9 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
traceMonitor: json["traceMonitor"]
|
||||
? (json["traceMonitor"] as JSONObject)
|
||||
: undefined,
|
||||
exceptionMonitor: json["exceptionMonitor"]
|
||||
? (json["exceptionMonitor"] as JSONObject)
|
||||
: undefined,
|
||||
}) as any;
|
||||
|
||||
if (monitorStep.data && !monitorStep.data?.logMonitor) {
|
||||
@@ -423,6 +448,11 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
MonitorStepMetricMonitorUtil.getDefault();
|
||||
}
|
||||
|
||||
if (monitorStep.data && !monitorStep.data?.exceptionMonitor) {
|
||||
monitorStep.data.exceptionMonitor =
|
||||
MonitorStepExceptionMonitorUtil.getDefault();
|
||||
}
|
||||
|
||||
return monitorStep;
|
||||
}
|
||||
|
||||
|
||||
94
Common/Types/Monitor/MonitorStepExceptionMonitor.ts
Normal file
94
Common/Types/Monitor/MonitorStepExceptionMonitor.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import ExceptionInstance from "../../Models/AnalyticsModels/ExceptionInstance";
|
||||
import InBetween from "../BaseDatabase/InBetween";
|
||||
import Includes from "../BaseDatabase/Includes";
|
||||
import Query from "../BaseDatabase/Query";
|
||||
import Search from "../BaseDatabase/Search";
|
||||
import OneUptimeDate from "../Date";
|
||||
import { JSONObject } from "../JSON";
|
||||
import ObjectID from "../ObjectID";
|
||||
|
||||
export default interface MonitorStepExceptionMonitor {
|
||||
telemetryServiceIds: Array<ObjectID>;
|
||||
exceptionTypes: Array<string>;
|
||||
message: string;
|
||||
includeResolved: boolean;
|
||||
includeArchived: boolean;
|
||||
lastXSecondsOfExceptions: number;
|
||||
}
|
||||
|
||||
export class MonitorStepExceptionMonitorUtil {
|
||||
public static toAnalyticsQuery(
|
||||
monitorStepExceptionMonitor: MonitorStepExceptionMonitor,
|
||||
): Query<ExceptionInstance> {
|
||||
const query: Query<ExceptionInstance> = {};
|
||||
|
||||
if (
|
||||
monitorStepExceptionMonitor.telemetryServiceIds &&
|
||||
monitorStepExceptionMonitor.telemetryServiceIds.length > 0
|
||||
) {
|
||||
query.serviceId = new Includes(
|
||||
monitorStepExceptionMonitor.telemetryServiceIds,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
monitorStepExceptionMonitor.exceptionTypes &&
|
||||
monitorStepExceptionMonitor.exceptionTypes.length > 0
|
||||
) {
|
||||
query.exceptionType = new Includes(
|
||||
monitorStepExceptionMonitor.exceptionTypes,
|
||||
);
|
||||
}
|
||||
|
||||
if (monitorStepExceptionMonitor.message) {
|
||||
query.message = new Search(monitorStepExceptionMonitor.message);
|
||||
}
|
||||
|
||||
if (monitorStepExceptionMonitor.lastXSecondsOfExceptions) {
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveSeconds(
|
||||
endDate,
|
||||
monitorStepExceptionMonitor.lastXSecondsOfExceptions * -1,
|
||||
);
|
||||
query.time = new InBetween(startDate, endDate);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public static getDefault(): MonitorStepExceptionMonitor {
|
||||
return {
|
||||
telemetryServiceIds: [],
|
||||
exceptionTypes: [],
|
||||
message: "",
|
||||
includeResolved: false,
|
||||
includeArchived: false,
|
||||
lastXSecondsOfExceptions: 60,
|
||||
};
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): MonitorStepExceptionMonitor {
|
||||
return {
|
||||
telemetryServiceIds: ObjectID.fromJSONArray(
|
||||
(json["telemetryServiceIds"] as Array<JSONObject>) || [],
|
||||
),
|
||||
exceptionTypes: (json["exceptionTypes"] as Array<string>) || [],
|
||||
message: (json["message"] as string) || "",
|
||||
includeResolved: Boolean(json["includeResolved"]) || false,
|
||||
includeArchived: Boolean(json["includeArchived"]) || false,
|
||||
lastXSecondsOfExceptions:
|
||||
(json["lastXSecondsOfExceptions"] as number | undefined) || 60,
|
||||
};
|
||||
}
|
||||
|
||||
public static toJSON(monitor: MonitorStepExceptionMonitor): JSONObject {
|
||||
return {
|
||||
telemetryServiceIds: ObjectID.toJSONArray(monitor.telemetryServiceIds),
|
||||
exceptionTypes: monitor.exceptionTypes,
|
||||
message: monitor.message,
|
||||
includeResolved: monitor.includeResolved,
|
||||
includeArchived: monitor.includeArchived,
|
||||
lastXSecondsOfExceptions: monitor.lastXSecondsOfExceptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ enum MonitorType {
|
||||
Logs = "Logs",
|
||||
Metrics = "Metrics",
|
||||
Traces = "Traces",
|
||||
Exceptions = "Exceptions",
|
||||
}
|
||||
|
||||
export default MonitorType;
|
||||
@@ -35,7 +36,8 @@ export class MonitorTypeHelper {
|
||||
return (
|
||||
monitorType === MonitorType.Logs ||
|
||||
monitorType === MonitorType.Metrics ||
|
||||
monitorType === MonitorType.Traces
|
||||
monitorType === MonitorType.Traces ||
|
||||
monitorType === MonitorType.Exceptions
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,6 +125,12 @@ export class MonitorTypeHelper {
|
||||
title: "Logs",
|
||||
description: "This monitor type lets you monitor logs from any source.",
|
||||
},
|
||||
{
|
||||
monitorType: MonitorType.Exceptions,
|
||||
title: "Exceptions",
|
||||
description:
|
||||
"This monitor type lets you monitor exceptions and error groups from any source.",
|
||||
},
|
||||
{
|
||||
monitorType: MonitorType.Traces,
|
||||
title: "Traces",
|
||||
@@ -198,6 +206,7 @@ export class MonitorTypeHelper {
|
||||
MonitorType.Logs,
|
||||
MonitorType.Metrics,
|
||||
MonitorType.Traces,
|
||||
MonitorType.Exceptions,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ import Span from "../../Models/AnalyticsModels/Span";
|
||||
import Query from "../BaseDatabase/Query";
|
||||
import MetricViewData from "../Metrics/MetricViewData";
|
||||
import TelemetryType from "./TelemetryType";
|
||||
import ExceptionInstance from "../../Models/AnalyticsModels/ExceptionInstance";
|
||||
|
||||
export interface TelemetryQuery {
|
||||
telemetryType: TelemetryType;
|
||||
telemetryQuery: Query<Log> | Query<Span> | null;
|
||||
telemetryQuery: Query<Log> | Query<Span> | Query<ExceptionInstance> | null;
|
||||
metricViewData: MetricViewData | null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ enum TelemetryType {
|
||||
Metric = "Metric",
|
||||
Trace = "Trace",
|
||||
Log = "Log",
|
||||
Exception = "Exception",
|
||||
}
|
||||
|
||||
export default TelemetryType;
|
||||
|
||||
@@ -297,4 +297,19 @@ export default class Text {
|
||||
): string {
|
||||
return sentence.split(search).join(replaceBy);
|
||||
}
|
||||
|
||||
public static truncate(
|
||||
value: string | null | undefined,
|
||||
maxLength: number,
|
||||
): string | undefined {
|
||||
if (value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (maxLength <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return value.length > maxLength ? value.slice(0, maxLength) : value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,32 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import Select, { ControlProps, GroupBase, OptionProps } from "react-select";
|
||||
import Color from "../../../Types/Color";
|
||||
import Label from "../../../Models/DatabaseModels/Label";
|
||||
import Select, {
|
||||
ControlProps,
|
||||
CSSObjectWithLabel,
|
||||
FormatOptionLabelMeta,
|
||||
GroupBase,
|
||||
OptionProps,
|
||||
} from "react-select";
|
||||
|
||||
export type DropdownValue = string | number | boolean;
|
||||
|
||||
export type DropdownOptionLabel =
|
||||
| Label
|
||||
| {
|
||||
id?: string;
|
||||
name: string;
|
||||
color?: Color;
|
||||
};
|
||||
|
||||
export interface DropdownOption {
|
||||
value: DropdownValue;
|
||||
label: string;
|
||||
description?: string;
|
||||
labels?: Array<DropdownOptionLabel>;
|
||||
color?: Color;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
@@ -111,6 +130,333 @@ const Dropdown: FunctionComponent<ComponentProps> = (
|
||||
|
||||
const firstUpdate: React.MutableRefObject<boolean> = useRef(true);
|
||||
|
||||
interface NormalizedDropdownLabel {
|
||||
id?: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const normalizeLabelColor: (
|
||||
color?: Color | string | null,
|
||||
) => string | undefined = (
|
||||
color?: Color | string | null,
|
||||
): string | undefined => {
|
||||
if (!color) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (color instanceof Color) {
|
||||
return color.toString();
|
||||
}
|
||||
|
||||
if (typeof color === "string" && color.trim().length > 0) {
|
||||
return color;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const normalizeDropdownLabel: (
|
||||
label: DropdownOptionLabel,
|
||||
) => NormalizedDropdownLabel | null = (
|
||||
label: DropdownOptionLabel,
|
||||
): NormalizedDropdownLabel | null => {
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getValueFromModel: (
|
||||
columnName: string,
|
||||
) => string | Color | null | undefined = (
|
||||
columnName: string,
|
||||
): string | Color | null | undefined => {
|
||||
if (
|
||||
typeof (label as Label).getColumnValue === "function" &&
|
||||
typeof (label as Label).getTableColumnMetadata === "function"
|
||||
) {
|
||||
return (label as Label).getColumnValue(columnName) as
|
||||
| string
|
||||
| Color
|
||||
| null
|
||||
| undefined;
|
||||
}
|
||||
|
||||
return (label as any)?.[columnName] as string | Color | null | undefined;
|
||||
};
|
||||
|
||||
const labelName: string | undefined = (() => {
|
||||
const valueFromGetter: string | null | undefined = getValueFromModel(
|
||||
"name",
|
||||
) as string | undefined | null;
|
||||
|
||||
if (valueFromGetter && valueFromGetter.trim().length > 0) {
|
||||
return valueFromGetter;
|
||||
}
|
||||
|
||||
const fallbackName: string | undefined = (label as any)?.name;
|
||||
if (fallbackName && fallbackName.trim().length > 0) {
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
if (!labelName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawColor: Color | string | null | undefined = getValueFromModel(
|
||||
"color",
|
||||
) as Color | string | null | undefined;
|
||||
const color: string | undefined =
|
||||
normalizeLabelColor(rawColor) ||
|
||||
normalizeLabelColor((label as any)?.color);
|
||||
|
||||
const idValue: string | undefined = (() => {
|
||||
if (typeof (label as Label).id !== "undefined") {
|
||||
const idFromGetter: ObjectID | null | undefined = (label as Label).id;
|
||||
if (idFromGetter) {
|
||||
return idFromGetter.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackId: string | undefined =
|
||||
(label as any)?._id || (label as any)?.id;
|
||||
|
||||
if (fallbackId) {
|
||||
return fallbackId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
const normalized: NormalizedDropdownLabel = {
|
||||
name: labelName,
|
||||
};
|
||||
|
||||
if (idValue) {
|
||||
normalized.id = idValue;
|
||||
}
|
||||
|
||||
if (color) {
|
||||
normalized.color = color;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const normalizeLabelCollection: (
|
||||
labels?: Array<DropdownOptionLabel>,
|
||||
) => Array<NormalizedDropdownLabel> = (
|
||||
labels?: Array<DropdownOptionLabel>,
|
||||
): Array<NormalizedDropdownLabel> => {
|
||||
if (!labels || labels.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return labels
|
||||
.map((label: DropdownOptionLabel) => {
|
||||
return normalizeDropdownLabel(label);
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
label: NormalizedDropdownLabel | null,
|
||||
): label is NormalizedDropdownLabel => {
|
||||
return label !== null;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const renderOptionColorIndicator: (
|
||||
color?: Color | string,
|
||||
) => ReactElement | null = (color?: Color | string): ReactElement | null => {
|
||||
const normalizedColor: string | undefined = color
|
||||
? new Color(color).toString()
|
||||
: undefined;
|
||||
|
||||
if (!normalizedColor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-2.5 w-2.5 flex-none rounded-full border border-gray-200"
|
||||
style={{
|
||||
backgroundColor: normalizedColor,
|
||||
}}
|
||||
title={normalizedColor}
|
||||
></span>
|
||||
);
|
||||
};
|
||||
|
||||
const getLabelStyle: (color?: string) => {
|
||||
backgroundColor: string;
|
||||
color: string;
|
||||
} = (color?: string): { backgroundColor: string; color: string } => {
|
||||
if (!color) {
|
||||
return {
|
||||
backgroundColor: "#e5e7eb", // gray-200
|
||||
color: "#374151", // gray-700
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedColor: Color = Color.fromString(color);
|
||||
return {
|
||||
backgroundColor: parsedColor.toString(),
|
||||
color: Color.shouldUseDarkText(parsedColor)
|
||||
? "#111827" // gray-900
|
||||
: "#f9fafb", // gray-50
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
backgroundColor: color,
|
||||
color: "#111827",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const defaultSelectedLabelAccentColor: string = "#6366f1"; // indigo-500
|
||||
|
||||
const resolveSelectedLabelColor: (color?: string) => string = (
|
||||
color?: string,
|
||||
): string => {
|
||||
if (!color) {
|
||||
return defaultSelectedLabelAccentColor;
|
||||
}
|
||||
|
||||
try {
|
||||
return Color.fromString(color).toString();
|
||||
} catch {
|
||||
return defaultSelectedLabelAccentColor;
|
||||
}
|
||||
};
|
||||
|
||||
const renderAssociatedLabels: (
|
||||
labels: Array<NormalizedDropdownLabel>,
|
||||
context: FormatOptionLabelMeta<DropdownOption>["context"],
|
||||
hiddenLabelCount: number,
|
||||
) => ReactElement | null = (
|
||||
labels: Array<NormalizedDropdownLabel>,
|
||||
context: FormatOptionLabelMeta<DropdownOption>["context"],
|
||||
hiddenLabelCount: number,
|
||||
): ReactElement | null => {
|
||||
if (labels.length === 0 && hiddenLabelCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context === "value") {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{labels.map((label: NormalizedDropdownLabel, index: number) => {
|
||||
const accentColor: string = resolveSelectedLabelColor(label.color);
|
||||
|
||||
return (
|
||||
<span
|
||||
key={`${label.id || label.name}-selected-${index}`}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-600"
|
||||
style={{
|
||||
borderColor: accentColor,
|
||||
}}
|
||||
title={label.name}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: accentColor,
|
||||
}}
|
||||
></span>
|
||||
{label.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{hiddenLabelCount > 0 ? (
|
||||
<span className="inline-flex items-center rounded-full border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-500">
|
||||
+{hiddenLabelCount}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{labels.map((label: NormalizedDropdownLabel, index: number) => {
|
||||
const { backgroundColor, color } = getLabelStyle(label.color);
|
||||
|
||||
return (
|
||||
<span
|
||||
key={`${label.id || label.name}-menu-${index}`}
|
||||
className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium shadow-sm"
|
||||
style={{ backgroundColor, color }}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{hiddenLabelCount > 0 ? (
|
||||
<span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">
|
||||
+{hiddenLabelCount}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const formatDropdownOptionLabel: (
|
||||
option: DropdownOption,
|
||||
meta: FormatOptionLabelMeta<DropdownOption>,
|
||||
) => ReactElement = (
|
||||
option: DropdownOption,
|
||||
meta: FormatOptionLabelMeta<DropdownOption>,
|
||||
): ReactElement => {
|
||||
const normalizedLabels: Array<NormalizedDropdownLabel> =
|
||||
normalizeLabelCollection(option.labels);
|
||||
|
||||
const maxVisibleLabels: number = meta.context === "menu" ? 4 : 2;
|
||||
const visibleLabels: Array<NormalizedDropdownLabel> =
|
||||
normalizedLabels.slice(0, maxVisibleLabels);
|
||||
const hiddenLabelCount: number = Math.max(
|
||||
normalizedLabels.length - visibleLabels.length,
|
||||
0,
|
||||
);
|
||||
|
||||
if (meta.context === "value") {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{renderOptionColorIndicator(option.color)}
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
{renderAssociatedLabels(
|
||||
visibleLabels,
|
||||
meta.context,
|
||||
hiddenLabelCount,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{renderOptionColorIndicator(option.color)}
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
{option.description ? (
|
||||
<span className="text-xs text-gray-500">{option.description}</span>
|
||||
) : null}
|
||||
{renderAssociatedLabels(visibleLabels, meta.context, hiddenLabelCount)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (firstUpdate.current && props.initialValue) {
|
||||
firstUpdate.current = false;
|
||||
@@ -138,6 +484,9 @@ const Dropdown: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
classNamePrefix="ou-select"
|
||||
unstyled={false}
|
||||
formatOptionLabel={formatDropdownOptionLabel}
|
||||
onBlur={() => {
|
||||
props.onBlur?.();
|
||||
}}
|
||||
@@ -150,25 +499,169 @@ const Dropdown: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
classNames={{
|
||||
control: (
|
||||
state: ControlProps<any, boolean, GroupBase<any>>,
|
||||
state: ControlProps<DropdownOption, boolean, GroupBase<any>>,
|
||||
): string => {
|
||||
return state.isFocused
|
||||
? "!border-indigo-500"
|
||||
: "border-Gray500-300";
|
||||
const classes: Array<string> = [
|
||||
"!min-h-[40px] !rounded-lg !border !bg-white !shadow-sm !transition-all !duration-150",
|
||||
state.isFocused
|
||||
? "!border-indigo-400 !ring-2 !ring-indigo-100"
|
||||
: "!border-gray-300 hover:!border-indigo-300",
|
||||
state.isDisabled
|
||||
? "!bg-gray-100 !text-gray-400"
|
||||
: "!cursor-pointer",
|
||||
];
|
||||
|
||||
if (props.error) {
|
||||
classes.push("!border-red-400 !ring-2 !ring-red-100");
|
||||
}
|
||||
|
||||
return classes.join(" ");
|
||||
},
|
||||
valueContainer: () => {
|
||||
return "!gap-2 !px-2";
|
||||
},
|
||||
placeholder: () => {
|
||||
return "text-sm text-gray-400";
|
||||
},
|
||||
input: () => {
|
||||
return "text-sm text-gray-900";
|
||||
},
|
||||
singleValue: () => {
|
||||
return "text-sm text-gray-900 font-medium";
|
||||
},
|
||||
indicatorsContainer: () => {
|
||||
return "!gap-1 !px-1";
|
||||
},
|
||||
dropdownIndicator: () => {
|
||||
return "text-gray-500 transition-colors duration-150 hover:text-indigo-400";
|
||||
},
|
||||
clearIndicator: () => {
|
||||
return "text-gray-400 transition-colors duration-150 hover:text-red-500";
|
||||
},
|
||||
menu: () => {
|
||||
return "!mt-2 !rounded-xl !border !border-gray-100 !bg-white !shadow-xl";
|
||||
},
|
||||
menuList: () => {
|
||||
return "!py-2";
|
||||
},
|
||||
option: (
|
||||
state: OptionProps<any, boolean, GroupBase<any>>,
|
||||
state: OptionProps<DropdownOption, boolean, GroupBase<any>>,
|
||||
): string => {
|
||||
if (state.isDisabled) {
|
||||
return "bg-gray-100";
|
||||
return "px-3 py-2 text-sm text-gray-300 cursor-not-allowed";
|
||||
}
|
||||
|
||||
if (state.isSelected) {
|
||||
return "!bg-indigo-500";
|
||||
return "px-3 py-2 text-sm bg-indigo-200 text-indigo-900";
|
||||
}
|
||||
|
||||
if (state.isFocused) {
|
||||
return "!bg-indigo-100";
|
||||
return "px-3 py-2 text-sm bg-indigo-100 text-indigo-700";
|
||||
}
|
||||
return "";
|
||||
|
||||
return "px-3 py-2 text-sm text-gray-700";
|
||||
},
|
||||
noOptionsMessage: () => {
|
||||
return "px-3 py-2 text-sm text-gray-500";
|
||||
},
|
||||
multiValue: () => {
|
||||
return "flex items-center gap-2 rounded-lg border border-indigo-100 bg-indigo-50 px-2 py-1";
|
||||
},
|
||||
multiValueLabel: () => {
|
||||
return "flex flex-wrap items-center gap-2 text-sm font-medium text-indigo-900";
|
||||
},
|
||||
multiValueRemove: () => {
|
||||
return "text-indigo-400 hover:text-indigo-600 transition-colors duration-150";
|
||||
},
|
||||
}}
|
||||
styles={{
|
||||
dropdownIndicator: (
|
||||
provided: CSSObjectWithLabel,
|
||||
): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
padding: 8,
|
||||
};
|
||||
},
|
||||
clearIndicator: (
|
||||
provided: CSSObjectWithLabel,
|
||||
): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
padding: 8,
|
||||
};
|
||||
},
|
||||
indicatorSeparator: (): CSSObjectWithLabel => {
|
||||
return {
|
||||
display: "none",
|
||||
} as CSSObjectWithLabel;
|
||||
},
|
||||
option: (
|
||||
provided: CSSObjectWithLabel,
|
||||
state: OptionProps<
|
||||
DropdownOption,
|
||||
boolean,
|
||||
GroupBase<DropdownOption>
|
||||
>,
|
||||
): CSSObjectWithLabel => {
|
||||
if (state.isSelected) {
|
||||
return {
|
||||
...provided,
|
||||
backgroundColor: "#c7d2fe", // indigo-200
|
||||
color: "#1e1b4b", // indigo-900
|
||||
};
|
||||
}
|
||||
|
||||
if (state.isFocused) {
|
||||
return {
|
||||
...provided,
|
||||
backgroundColor: "#e0e7ff", // indigo-100
|
||||
color: "#312e81", // indigo-800
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...provided,
|
||||
color: "#374151", // gray-700
|
||||
};
|
||||
},
|
||||
multiValue: (provided: CSSObjectWithLabel): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
backgroundColor: "#eef2ff", // indigo-50
|
||||
borderRadius: 8,
|
||||
border: "1px solid #c7d2fe", // indigo-200
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
};
|
||||
},
|
||||
multiValueLabel: (
|
||||
provided: CSSObjectWithLabel,
|
||||
): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
color: "#312e81", // indigo-800
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 500,
|
||||
};
|
||||
},
|
||||
multiValueRemove: (
|
||||
provided: CSSObjectWithLabel,
|
||||
): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
color: "#6366f1", // indigo-500
|
||||
":hover": {
|
||||
color: "#4f46e5", // indigo-600
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
};
|
||||
},
|
||||
menuPortal: (base: CSSObjectWithLabel): CSSObjectWithLabel => {
|
||||
return {
|
||||
...base,
|
||||
zIndex: 50,
|
||||
};
|
||||
},
|
||||
}}
|
||||
isClearable={true}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CategoryCheckboxOption,
|
||||
CheckboxCategory,
|
||||
} from "../CategoryCheckbox/CategoryCheckboxTypes";
|
||||
import type { DropdownOption } from "../Dropdown/Dropdown";
|
||||
import Loader, { LoaderType } from "../Loader/Loader";
|
||||
import Pill, { PillSize } from "../Pill/Pill";
|
||||
import { FormErrors, FormProps, FormSummaryConfig } from "./BasicForm";
|
||||
@@ -24,6 +25,7 @@ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseMod
|
||||
import AccessControlModel from "../../../Models/DatabaseModels/DatabaseBaseModel/AccessControlModel";
|
||||
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import FileModel from "../../../Models/DatabaseModels/DatabaseBaseModel/FileModel";
|
||||
import Label from "../../../Models/DatabaseModels/Label";
|
||||
import URL from "../../../Types/API/URL";
|
||||
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
|
||||
import { Black, VeryLightGray } from "../../../Types/BrandColors";
|
||||
@@ -412,17 +414,26 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
[field.dropdownModal.valueField]: true,
|
||||
} as any;
|
||||
|
||||
let hasAccessControlColumn: boolean = false;
|
||||
let colorColumnName: string | null = null;
|
||||
let shouldSelectColorColumn: boolean = false;
|
||||
|
||||
colorColumnName = tempModel.getFirstColorColumn();
|
||||
|
||||
if (colorColumnName) {
|
||||
select[colorColumnName] = true;
|
||||
shouldSelectColorColumn = true;
|
||||
}
|
||||
|
||||
const accessControlColumnName: string | null =
|
||||
tempModel.getAccessControlColumn();
|
||||
|
||||
// also select labels, so they can select resources by labels. This is useful for resources like monitors, etc.
|
||||
if (tempModel.getAccessControlColumn()) {
|
||||
select[tempModel.getAccessControlColumn()!] = {
|
||||
if (accessControlColumnName) {
|
||||
select[accessControlColumnName] = {
|
||||
_id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
} as any;
|
||||
|
||||
hasAccessControlColumn = true;
|
||||
}
|
||||
|
||||
const listResult: ListResult<BaseModel> =
|
||||
@@ -436,22 +447,51 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
});
|
||||
|
||||
if (listResult.data && listResult.data.length > 0) {
|
||||
field.dropdownOptions = listResult.data.map((item: BaseModel) => {
|
||||
if (!field.dropdownModal) {
|
||||
throw new BadDataException("Dropdown Modal value mot found");
|
||||
}
|
||||
const dropdownOptions: Array<DropdownOption> = listResult.data.map(
|
||||
(item: BaseModel) => {
|
||||
if (!field.dropdownModal) {
|
||||
throw new BadDataException("Dropdown Modal value mot found");
|
||||
}
|
||||
|
||||
return {
|
||||
label: (item as any)[
|
||||
field.dropdownModal?.labelField
|
||||
].toString(),
|
||||
value: (item as any)[
|
||||
field.dropdownModal?.valueField
|
||||
].toString(),
|
||||
};
|
||||
});
|
||||
const option: DropdownOption = {
|
||||
label: (item as any)[
|
||||
field.dropdownModal?.labelField
|
||||
].toString(),
|
||||
value: (item as any)[
|
||||
field.dropdownModal?.valueField
|
||||
].toString(),
|
||||
};
|
||||
|
||||
if (hasAccessControlColumn) {
|
||||
if (colorColumnName && shouldSelectColorColumn) {
|
||||
const color: Color = item.getColumnValue(
|
||||
colorColumnName,
|
||||
) as Color;
|
||||
if (color) {
|
||||
option.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
if (accessControlColumnName) {
|
||||
const labelsForItem: Array<AccessControlModel> = (
|
||||
((item as any)[
|
||||
accessControlColumnName
|
||||
] as Array<AccessControlModel>) || []
|
||||
).filter((label: AccessControlModel | null) => {
|
||||
return Boolean(label);
|
||||
}) as Array<AccessControlModel>;
|
||||
|
||||
if (labelsForItem.length > 0) {
|
||||
option.labels = labelsForItem as Array<Label>;
|
||||
}
|
||||
}
|
||||
|
||||
return option;
|
||||
},
|
||||
);
|
||||
|
||||
field.dropdownOptions = dropdownOptions;
|
||||
|
||||
if (accessControlColumnName) {
|
||||
const categories: Array<CheckboxCategory> = [];
|
||||
|
||||
// populate categories.
|
||||
@@ -459,8 +499,7 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
let localLabels: Array<AccessControlModel> = [];
|
||||
|
||||
for (const item of listResult.data) {
|
||||
const accessControlColumn: string | null =
|
||||
tempModel.getAccessControlColumn()!;
|
||||
const accessControlColumn: string = accessControlColumnName;
|
||||
const labels: Array<AccessControlModel> =
|
||||
((item as any)[
|
||||
accessControlColumn
|
||||
@@ -516,8 +555,7 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
const options: Array<CategoryCheckboxOption> = [];
|
||||
|
||||
for (const item of listResult.data) {
|
||||
const accessControlColumn: string =
|
||||
tempModel.getAccessControlColumn()!;
|
||||
const accessControlColumn: string = accessControlColumnName;
|
||||
const labels: Array<AccessControlModel> =
|
||||
((item as any)[
|
||||
accessControlColumn
|
||||
@@ -554,9 +592,8 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
options: options,
|
||||
},
|
||||
accessControlColumnTitle:
|
||||
tempModel.getTableColumnMetadata(
|
||||
tempModel.getAccessControlColumn()!,
|
||||
).title || "",
|
||||
tempModel.getTableColumnMetadata(accessControlColumnName)
|
||||
.title || "",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1102,6 +1102,10 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>,
|
||||
);
|
||||
} else if (icon === IconProp.EmptyCircle) {
|
||||
return getSvgWrapper(<circle cx="12" cy="12" r="7.5" />, {
|
||||
strokeWidth: "2.25",
|
||||
});
|
||||
} else if (icon === IconProp.Circle) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
|
||||
50
Common/UI/Components/Label/Label.tsx
Normal file
50
Common/UI/Components/Label/Label.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { CSSProperties, FunctionComponent, ReactElement } from "react";
|
||||
import LabelModel from "../../../Models/DatabaseModels/Label";
|
||||
import Pill, { ComponentProps as PillProps, PillSize } from "../Pill/Pill";
|
||||
import { Black } from "../../../Types/BrandColors";
|
||||
import Color from "../../../Types/Color";
|
||||
import IconProp from "../../../Types/Icon/IconProp";
|
||||
|
||||
export interface ComponentProps {
|
||||
label: LabelModel;
|
||||
size?: PillSize | undefined;
|
||||
style?: CSSProperties;
|
||||
isMinimal?: boolean | undefined;
|
||||
}
|
||||
|
||||
const LabelElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const { label } = props;
|
||||
|
||||
const resolveColor: Color = (() => {
|
||||
if (!label.color) {
|
||||
return Black;
|
||||
}
|
||||
|
||||
if (typeof label.color === "string") {
|
||||
return Color.fromString(label.color);
|
||||
}
|
||||
|
||||
return label.color;
|
||||
})();
|
||||
|
||||
const text: string = label.name || label.slug || "";
|
||||
|
||||
const pillProps: PillProps = {
|
||||
color: resolveColor,
|
||||
text,
|
||||
size: props.size,
|
||||
isMinimal: props.isMinimal,
|
||||
tooltip: label.description || undefined,
|
||||
icon: IconProp.EmptyCircle,
|
||||
};
|
||||
|
||||
if (props.style) {
|
||||
pillProps.style = props.style;
|
||||
}
|
||||
|
||||
return <Pill {...pillProps} />;
|
||||
};
|
||||
|
||||
export default LabelElement;
|
||||
@@ -1,25 +1,29 @@
|
||||
import LabelElement from "./Label";
|
||||
import TableColumnListComponent from "Common/UI/Components/TableColumnList/TableColumnListComponent";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import TableColumnListComponent from "../TableColumnList/TableColumnListComponent";
|
||||
import LabelModel from "../../../Models/DatabaseModels/Label";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
labels: Array<Label>;
|
||||
labels: Array<LabelModel>;
|
||||
}
|
||||
|
||||
const LabelsElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
// {/** >4 because 3 labels are shown by default and then the more text is shown */}
|
||||
<TableColumnListComponent
|
||||
items={props.labels}
|
||||
moreText={props.labels.length > 4 ? "more labels" : "more label"}
|
||||
className={props.labels.length > 0 ? "-mb-1 -mt-1" : ""}
|
||||
getEachElement={(label: Label) => {
|
||||
getEachElement={(label: LabelModel) => {
|
||||
return (
|
||||
<div className={props.labels.length > 0 ? "my-2" : ""}>
|
||||
<LabelElement label={label} />
|
||||
<LabelElement
|
||||
label={label}
|
||||
style={{
|
||||
marginRight: "5px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@@ -38,6 +38,7 @@ import { ListDetailProps } from "../List/ListRow";
|
||||
import ConfirmModal from "../Modal/ConfirmModal";
|
||||
import { ModalWidth } from "../Modal/Modal";
|
||||
import Filter from "../ModelFilter/Filter";
|
||||
import { DropdownOption, DropdownOptionLabel } from "../Dropdown/Dropdown";
|
||||
import OrderedStatesList from "../OrderedStatesList/OrderedStatesList";
|
||||
import Pill from "../Pill/Pill";
|
||||
import Table from "../Table/Table";
|
||||
@@ -51,6 +52,7 @@ import AnalyticsBaseModel, {
|
||||
import BaseModel, {
|
||||
DatabaseBaseModelType,
|
||||
} from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AccessControlModel from "../../../Models/DatabaseModels/DatabaseBaseModel/AccessControlModel";
|
||||
import Route from "../../../Types/API/Route";
|
||||
import URL from "../../../Types/API/URL";
|
||||
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
|
||||
@@ -64,6 +66,7 @@ import { Yellow } from "../../../Types/BrandColors";
|
||||
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
import Color from "../../../Types/Color";
|
||||
import {
|
||||
ErrorFunction,
|
||||
PromiseVoidFunction,
|
||||
@@ -641,30 +644,155 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
||||
|
||||
const query: Query<TBaseModel> = filter.filterQuery || {};
|
||||
|
||||
let colorColumnName: string | null = null;
|
||||
let accessControlColumnName: string | null = null;
|
||||
|
||||
if (
|
||||
filter.filterEntityType &&
|
||||
filter.filterEntityType.prototype instanceof BaseModel
|
||||
) {
|
||||
const filterModel: BaseModel =
|
||||
new (filter.filterEntityType as DatabaseBaseModelType)();
|
||||
colorColumnName = filterModel.getFirstColorColumn();
|
||||
accessControlColumnName = filterModel.getAccessControlColumn();
|
||||
}
|
||||
|
||||
const select: Select<TBaseModel> = {
|
||||
[filter.filterDropdownField.label]: true,
|
||||
[filter.filterDropdownField.value]: true,
|
||||
} as Select<TBaseModel>;
|
||||
|
||||
if (colorColumnName) {
|
||||
(select as Dictionary<boolean>)[colorColumnName] = true;
|
||||
}
|
||||
|
||||
if (accessControlColumnName) {
|
||||
(select as Dictionary<JSONObject>)[accessControlColumnName] = {
|
||||
_id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
} as JSONObject;
|
||||
}
|
||||
|
||||
const listResult: ListResult<TBaseModel> =
|
||||
await props.callbacks.getList({
|
||||
modelType: filter.filterEntityType,
|
||||
query: query,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
[filter.filterDropdownField.label]: true,
|
||||
[filter.filterDropdownField.value]: true,
|
||||
} as any,
|
||||
select: select,
|
||||
sort: {},
|
||||
});
|
||||
|
||||
filter.filterDropdownOptions = [];
|
||||
|
||||
for (const item of listResult.data) {
|
||||
filter.filterDropdownOptions.push({
|
||||
const option: DropdownOption = {
|
||||
value: item.getColumnValue(
|
||||
filter.filterDropdownField.value,
|
||||
) as string,
|
||||
label: item.getColumnValue(
|
||||
filter.filterDropdownField.label,
|
||||
) as string,
|
||||
});
|
||||
};
|
||||
|
||||
if (colorColumnName) {
|
||||
const colorValue: Color | string | null = item.getColumnValue(
|
||||
colorColumnName,
|
||||
) as Color | string | null;
|
||||
|
||||
if (colorValue instanceof Color) {
|
||||
option.color = colorValue;
|
||||
} else if (
|
||||
typeof colorValue === "string" &&
|
||||
colorValue.trim().length > 0
|
||||
) {
|
||||
try {
|
||||
option.color = new Color(colorValue);
|
||||
} catch {
|
||||
// ignore invalid colors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (accessControlColumnName) {
|
||||
const accessControlValue:
|
||||
| AccessControlModel
|
||||
| Array<AccessControlModel>
|
||||
| null =
|
||||
(item.getColumnValue(accessControlColumnName) as
|
||||
| AccessControlModel
|
||||
| Array<AccessControlModel>
|
||||
| null) || null;
|
||||
|
||||
const accessControlItems: Array<AccessControlModel> =
|
||||
Array.isArray(accessControlValue)
|
||||
? accessControlValue
|
||||
: accessControlValue
|
||||
? [accessControlValue]
|
||||
: [];
|
||||
|
||||
type SimplifiedDropdownLabel = {
|
||||
id?: string;
|
||||
name: string;
|
||||
color?: Color;
|
||||
};
|
||||
|
||||
const dropdownLabels: Array<SimplifiedDropdownLabel> =
|
||||
accessControlItems
|
||||
.map((label: AccessControlModel | null) => {
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelNameRaw: string | null = label.getColumnValue(
|
||||
"name",
|
||||
) as string | null;
|
||||
|
||||
if (!labelNameRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelName: string = labelNameRaw.toString().trim();
|
||||
|
||||
if (!labelName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelColorValue: Color | null = label.getColumnValue(
|
||||
"color",
|
||||
) as Color | null;
|
||||
|
||||
const normalizedLabel: SimplifiedDropdownLabel = {
|
||||
name: labelName,
|
||||
};
|
||||
|
||||
const labelId: ObjectID | null = label.id;
|
||||
|
||||
if (labelId) {
|
||||
normalizedLabel.id = labelId.toString();
|
||||
}
|
||||
|
||||
if (labelColorValue) {
|
||||
normalizedLabel.color = labelColorValue;
|
||||
}
|
||||
|
||||
return normalizedLabel;
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
label: SimplifiedDropdownLabel | null,
|
||||
): label is SimplifiedDropdownLabel => {
|
||||
return label !== null;
|
||||
},
|
||||
);
|
||||
|
||||
if (dropdownLabels.length > 0) {
|
||||
option.labels = dropdownLabels as Array<DropdownOptionLabel>;
|
||||
}
|
||||
}
|
||||
|
||||
filter.filterDropdownOptions.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ import Analytics from "../../Utils/Analytics";
|
||||
import Breadcrumbs from "../Breadcrumbs/Breadcrumbs";
|
||||
import ErrorMessage from "../ErrorMessage/ErrorMessage";
|
||||
import PageLoader from "../Loader/PageLoader";
|
||||
import Pill from "../Pill/Pill";
|
||||
import LabelElement from "../Label/Label";
|
||||
import Link from "../../../Types/Link";
|
||||
import Label from "../../../Models/DatabaseModels/Label";
|
||||
import Color from "../../../Types/Color";
|
||||
import { Black } from "../../../Types/BrandColors";
|
||||
import LabelModel from "../../../Models/DatabaseModels/Label";
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
@@ -17,7 +15,7 @@ export interface ComponentProps {
|
||||
className?: string | undefined;
|
||||
isLoading?: boolean | undefined;
|
||||
error?: string | undefined;
|
||||
labels?: Array<Label> | undefined;
|
||||
labels?: Array<LabelModel> | undefined;
|
||||
}
|
||||
|
||||
const Page: FunctionComponent<ComponentProps> = (
|
||||
@@ -71,33 +69,19 @@ const Page: FunctionComponent<ComponentProps> = (
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-2 justify-end">
|
||||
{props.labels
|
||||
.filter((label: Label | null) => {
|
||||
.filter((label: LabelModel | null) => {
|
||||
return Boolean(label && (label.name || label.slug));
|
||||
})
|
||||
.map((label: Label, index: number) => {
|
||||
const resolveColor: Color = (() => {
|
||||
if (!label.color) {
|
||||
return Black;
|
||||
}
|
||||
|
||||
if (typeof label.color === "string") {
|
||||
return Color.fromString(label.color);
|
||||
}
|
||||
|
||||
return label.color;
|
||||
})();
|
||||
|
||||
.map((label: LabelModel, index: number) => {
|
||||
return (
|
||||
<Pill
|
||||
<LabelElement
|
||||
key={
|
||||
label.id?.toString() ||
|
||||
label._id ||
|
||||
label.slug ||
|
||||
`${label.name || "label"}-${index}`
|
||||
}
|
||||
color={resolveColor}
|
||||
text={label.name || label.slug || "Label"}
|
||||
isMinimal={false}
|
||||
label={label}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Black } from "../../../Types/BrandColors";
|
||||
import Color from "../../../Types/Color";
|
||||
import Icon, { SizeProp, ThickProp } from "../Icon/Icon";
|
||||
import IconProp from "../../../Types/Icon/IconProp";
|
||||
import React, { CSSProperties, FunctionComponent, ReactElement } from "react";
|
||||
import Tooltip from "../Tooltip/Tooltip";
|
||||
import { GetReactElementFunction } from "../../Types/FunctionTypes";
|
||||
@@ -18,6 +20,7 @@ export interface ComponentProps {
|
||||
style?: CSSProperties;
|
||||
isMinimal?: boolean | undefined;
|
||||
tooltip?: string | undefined;
|
||||
icon?: IconProp | undefined;
|
||||
}
|
||||
|
||||
const Pill: FunctionComponent<ComponentProps> = (
|
||||
@@ -47,7 +50,7 @@ const Pill: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<span
|
||||
data-testid="pill"
|
||||
className="rounded-full p-1 pl-3 pr-3"
|
||||
className="inline-flex items-center rounded-full p-1 pl-3 pr-3"
|
||||
style={{
|
||||
// https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
|
||||
|
||||
@@ -63,8 +66,15 @@ const Pill: FunctionComponent<ComponentProps> = (
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{props.text}{" "}
|
||||
{props.icon ? (
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
size={SizeProp.Small}
|
||||
thick={ThickProp.Thick}
|
||||
className="mr-2"
|
||||
/>
|
||||
) : null}
|
||||
{props.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import Navigation from "../Navigation";
|
||||
import PermissionUtil from "../Permission";
|
||||
import User from "../User";
|
||||
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
||||
import HTTPMethod from "../../../Types/API/HTTPMethod";
|
||||
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
||||
import Headers from "../../../Types/API/Headers";
|
||||
import Hostname from "../../../Types/API/Hostname";
|
||||
import Protocol from "../../../Types/API/Protocol";
|
||||
@@ -11,14 +13,18 @@ import URL from "../../../Types/API/URL";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import APIException from "../../../Types/Exception/ApiException";
|
||||
import Exception from "../../../Types/Exception/Exception";
|
||||
import { JSONObject } from "../../../Types/JSON";
|
||||
import JSONFunctions from "../../../Types/JSONFunctions";
|
||||
import {
|
||||
UserGlobalAccessPermission,
|
||||
UserTenantAccessPermission,
|
||||
} from "../../../Types/Permission";
|
||||
import API from "../../../Utils/API";
|
||||
import API, { AuthRetryContext } from "../../../Utils/API";
|
||||
import { IDENTITY_URL } from "../../Config";
|
||||
|
||||
class BaseAPI extends API {
|
||||
private static refreshPromise: Promise<boolean> | null = null;
|
||||
|
||||
public constructor(protocol: Protocol, hostname: Hostname, route?: Route) {
|
||||
super(protocol, hostname, route);
|
||||
}
|
||||
@@ -134,6 +140,38 @@ class BaseAPI extends API {
|
||||
return error;
|
||||
}
|
||||
|
||||
protected static override async tryRefreshAuth(
|
||||
_context: AuthRetryContext,
|
||||
): Promise<boolean> {
|
||||
if (!this.refreshPromise) {
|
||||
this.refreshPromise = (async () => {
|
||||
const refreshUrl: URL = URL.fromString(
|
||||
IDENTITY_URL.toString(),
|
||||
).addRoute("/refresh-token");
|
||||
|
||||
const result: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await super.fetch<JSONObject>({
|
||||
method: HTTPMethod.POST,
|
||||
url: refreshUrl,
|
||||
options: {
|
||||
skipAuthRefresh: true,
|
||||
hasAttemptedAuthRefresh: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (result instanceof HTTPResponse && result.isSuccess()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})().finally(() => {
|
||||
this.refreshPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return await this.refreshPromise;
|
||||
}
|
||||
|
||||
protected static getLoginRoute(): Route {
|
||||
return new Route("/accounts/login");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import NumberUtil from "../../Utils/Number";
|
||||
import { DropdownOption } from "../Components/Dropdown/Dropdown";
|
||||
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Color from "../../Types/Color";
|
||||
|
||||
type Enum<E> = Record<keyof E, number | string> & { [k: number]: string };
|
||||
|
||||
@@ -52,10 +53,27 @@ export default class DropdownUtil {
|
||||
valueField: string;
|
||||
}): Array<DropdownOption> {
|
||||
return data.array.map((item: TBaseModel) => {
|
||||
return {
|
||||
const option: DropdownOption = {
|
||||
label: item.getColumnValue(data.labelField) as string,
|
||||
value: item.getColumnValue(data.valueField) as string,
|
||||
};
|
||||
|
||||
const colorColumnName: string | null =
|
||||
typeof item.getFirstColorColumn === "function"
|
||||
? item.getFirstColorColumn()
|
||||
: null;
|
||||
|
||||
if (colorColumnName) {
|
||||
const color: Color | null = item.getColumnValue(
|
||||
colorColumnName,
|
||||
) as Color | null;
|
||||
|
||||
if (color) {
|
||||
option.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
TracerConfig,
|
||||
WebTracerProvider,
|
||||
} from "@opentelemetry/sdk-trace-web";
|
||||
import type { SpanExporter } from "@opentelemetry/sdk-trace-base";
|
||||
import type { SpanExporter as WebSpanExporter } from "@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/sdk-trace-base/build/src/export/SpanExporter";
|
||||
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
||||
import URL from "../../../Types/API/URL";
|
||||
|
||||
@@ -35,16 +37,17 @@ export default class Telemetry {
|
||||
|
||||
const provider: WebTracerProvider = new WebTracerProvider(providerConfig);
|
||||
|
||||
provider.addSpanProcessor(
|
||||
new BatchSpanProcessor(
|
||||
new OTLPTraceExporter({
|
||||
url: URL.fromString(
|
||||
OpenTelemetryExporterOtlpEndpoint?.toString() + "/v1/traces",
|
||||
).toString(),
|
||||
headers: OpenTelemetryExporterOtlpHeaders,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const traceExporter: SpanExporter = new OTLPTraceExporter({
|
||||
url: URL.fromString(
|
||||
OpenTelemetryExporterOtlpEndpoint?.toString() + "/v1/traces",
|
||||
).toString(),
|
||||
headers: OpenTelemetryExporterOtlpHeaders,
|
||||
}) as unknown as SpanExporter;
|
||||
|
||||
const webTraceExporter: WebSpanExporter =
|
||||
traceExporter as unknown as WebSpanExporter;
|
||||
|
||||
provider.addSpanProcessor(new BatchSpanProcessor(webTraceExporter));
|
||||
|
||||
provider.register({
|
||||
contextManager: new ZoneContextManager(),
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface RequestOptions {
|
||||
// Per-request proxy agent support (Probe supplies these instead of mutating global axios defaults)
|
||||
httpAgent?: HttpAgent | undefined;
|
||||
httpsAgent?: HttpsAgent | undefined;
|
||||
skipAuthRefresh?: boolean | undefined;
|
||||
hasAttemptedAuthRefresh?: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface APIRequestOptions {
|
||||
@@ -43,6 +45,18 @@ export interface APIFetchOptions {
|
||||
options?: RequestOptions;
|
||||
}
|
||||
|
||||
export interface AuthRetryContext {
|
||||
error: HTTPErrorResponse;
|
||||
request: {
|
||||
method: HTTPMethod;
|
||||
url: URL;
|
||||
data?: JSONObject | JSONArray;
|
||||
headers?: Headers;
|
||||
params?: Dictionary<string>;
|
||||
options?: RequestOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export default class API {
|
||||
private _protocol: Protocol = Protocol.HTTPS;
|
||||
public get protocol(): Protocol {
|
||||
@@ -83,6 +97,12 @@ export default class API {
|
||||
}
|
||||
}
|
||||
|
||||
protected static async tryRefreshAuth(
|
||||
_context: AuthRetryContext,
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async get<
|
||||
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
|
||||
>(options: APIRequestOptions): Promise<HTTPResponse<T> | HTTPErrorResponse> {
|
||||
@@ -421,14 +441,63 @@ export default class API {
|
||||
return response;
|
||||
} catch (e) {
|
||||
const error: Error | AxiosError = e as Error | AxiosError;
|
||||
let errorResponse: HTTPErrorResponse;
|
||||
if (axios.isAxiosError(error)) {
|
||||
// Do whatever you want with native error
|
||||
errorResponse = this.getErrorResponse(error);
|
||||
} else {
|
||||
|
||||
if (!axios.isAxiosError(error)) {
|
||||
throw new APIException(error.message);
|
||||
}
|
||||
|
||||
const errorResponse: HTTPErrorResponse = this.getErrorResponse(error);
|
||||
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
!options?.skipAuthRefresh &&
|
||||
!options?.hasAttemptedAuthRefresh
|
||||
) {
|
||||
const retryUrl: URL = URL.fromString(url.toString());
|
||||
|
||||
const requestContext: AuthRetryContext["request"] = {
|
||||
method,
|
||||
url: retryUrl,
|
||||
};
|
||||
|
||||
if (data) {
|
||||
requestContext.data = data;
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
requestContext.headers = headers;
|
||||
}
|
||||
|
||||
if (params) {
|
||||
requestContext.params = params;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
requestContext.options = options;
|
||||
}
|
||||
|
||||
const refreshed: boolean = await this.tryRefreshAuth({
|
||||
error: errorResponse,
|
||||
request: requestContext,
|
||||
});
|
||||
|
||||
if (refreshed) {
|
||||
const nextOptions: RequestOptions = {
|
||||
...(options || {}),
|
||||
hasAttemptedAuthRefresh: true,
|
||||
};
|
||||
|
||||
return await this.fetchInternal(
|
||||
method,
|
||||
retryUrl,
|
||||
data,
|
||||
headers,
|
||||
params,
|
||||
nextOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.handleError(errorResponse);
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
34984
Common/package-lock.json
generated
34984
Common/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,148 +1,147 @@
|
||||
{
|
||||
"name": "@oneuptime/common",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "cd .. && export $(grep -v '^#' config.env | xargs) && cd Common && node node_modules/.bin/jest --runInBand ./Tests --detectOpenHandles",
|
||||
"test-file": "cd .. && export $(grep -v '^#' config.env | xargs) && cd Common && node node_modules/.bin/jest --runInBand $1 --detectOpenHandles",
|
||||
"coverage": "jest --detectOpenHandles --coverage",
|
||||
"compile": "tsc",
|
||||
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
|
||||
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
|
||||
"debug:test": "cd .. && export $(grep -v '^#' config.env | xargs) && cd Common && node --inspect node_modules/.bin/jest --runInBand ./Tests --detectOpenHandles",
|
||||
"debug:test-file": "cd .. && export $(grep -v '^#' config.env | xargs) && cd Common && node --inspect node_modules/.bin/jest --runInBand $1 --detectOpenHandles"
|
||||
},
|
||||
"author": "OneUptime <hello@oneuptime.com> (https://oneuptime.com/)",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/cookie-parser": "^1.4.4",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/ejs": "^3.1.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "^28.1.4",
|
||||
"@types/json2csv": "^5.0.3",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/node": "^17.0.45",
|
||||
"@types/node-cron": "^3.0.7",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/react": "^18.2.38",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-big-calendar": "^1.8.5",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"@types/react-toggle": "^4.0.3",
|
||||
"jest": "^28.1.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-mock-extended": "^3.0.5",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"sass": "^1.89.2",
|
||||
"ts-jest": "^28.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.2",
|
||||
"@bull-board/express": "^5.21.4",
|
||||
"@clickhouse/client": "^1.10.1",
|
||||
"@elastic/elasticsearch": "^8.12.1",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"acme-client": "^5.3.0",
|
||||
"airtable": "^0.12.2",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.12.0",
|
||||
"botbuilder": "^4.23.3",
|
||||
"bullmq": "^5.61.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^4.8.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.4.4",
|
||||
"ejs": "^3.1.10",
|
||||
"elkjs": "^0.10.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"express": "^4.21.1",
|
||||
"formik": "^2.4.6",
|
||||
"history": "^5.3.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"json2csv": "^5.0.7",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"marked": "^12.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^7.0.7",
|
||||
"otpauth": "^9.3.1",
|
||||
"pg": "^8.7.3",
|
||||
"playwright": "^1.56.0",
|
||||
"posthog-js": "^1.275.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-big-calendar": "^1.13.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^16.0.0",
|
||||
"react-toggle": "^4.1.3",
|
||||
"reactflow": "^11.11.4",
|
||||
"recharts": "^2.12.7",
|
||||
"redis-semaphore": "^5.5.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"slackify-markdown": "^4.4.0",
|
||||
"slugify": "^1.6.5",
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twilio": "^4.22.0",
|
||||
"typeorm": "^0.3.26",
|
||||
"typeorm-extension": "^2.2.13",
|
||||
"universal-cookie": "^7.2.1",
|
||||
"use-async-effect": "^2.2.6",
|
||||
"uuid": "^8.3.2",
|
||||
"web-push": "^3.6.7",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
"name": "@oneuptime/common",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "cd .. && export $(grep -v '^#' config.env | xargs) && cd Common && node node_modules/.bin/jest --runInBand ./Tests --detectOpenHandles",
|
||||
"test-file": "cd .. && export $(grep -v '^#' config.env | xargs) && cd Common && node node_modules/.bin/jest --runInBand $1 --detectOpenHandles",
|
||||
"coverage": "jest --detectOpenHandles --coverage",
|
||||
"compile": "tsc",
|
||||
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
|
||||
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
|
||||
"debug:test": "cd .. && export $(grep -v '^#' config.env | xargs) && cd Common && node --inspect node_modules/.bin/jest --runInBand ./Tests --detectOpenHandles",
|
||||
"debug:test-file": "cd .. && export $(grep -v '^#' config.env | xargs) && cd Common && node --inspect node_modules/.bin/jest --runInBand $1 --detectOpenHandles"
|
||||
},
|
||||
"author": "OneUptime <hello@oneuptime.com> (https://oneuptime.com/)",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/cookie-parser": "^1.4.4",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/ejs": "^3.1.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "^28.1.4",
|
||||
"@types/json2csv": "^5.0.3",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/node": "^17.0.45",
|
||||
"@types/node-cron": "^3.0.7",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/react": "^18.2.38",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-big-calendar": "^1.8.5",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"@types/react-toggle": "^4.0.3",
|
||||
"jest": "^28.1.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-mock-extended": "^3.0.5",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"sass": "^1.89.2",
|
||||
"ts-jest": "^28.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.2",
|
||||
"@bull-board/express": "^5.21.4",
|
||||
"@clickhouse/client": "^1.10.1",
|
||||
"@elastic/elasticsearch": "^8.12.1",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"acme-client": "^5.3.0",
|
||||
"airtable": "^0.12.2",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.12.0",
|
||||
"botbuilder": "^4.23.3",
|
||||
"bullmq": "^5.61.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^4.8.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.4.4",
|
||||
"ejs": "^3.1.10",
|
||||
"elkjs": "^0.10.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"express": "^4.21.1",
|
||||
"formik": "^2.4.6",
|
||||
"history": "^5.3.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"json2csv": "^5.0.7",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"marked": "^12.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^7.0.7",
|
||||
"otpauth": "^9.3.1",
|
||||
"pg": "^8.7.3",
|
||||
"playwright": "^1.56.0",
|
||||
"posthog-js": "^1.275.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-big-calendar": "^1.13.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^16.0.0",
|
||||
"react-toggle": "^4.1.3",
|
||||
"reactflow": "^11.11.4",
|
||||
"recharts": "^2.12.7",
|
||||
"redis-semaphore": "^5.5.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"slackify-markdown": "^4.4.0",
|
||||
"slugify": "^1.6.5",
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twilio": "^4.22.0",
|
||||
"typeorm": "^0.3.26",
|
||||
"typeorm-extension": "^2.2.13",
|
||||
"universal-cookie": "^7.2.1",
|
||||
"use-async-effect": "^2.2.6",
|
||||
"uuid": "^8.3.2",
|
||||
"web-push": "^3.6.7",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
}
|
||||
|
||||
19
Copilot/package-lock.json
generated
19
Copilot/package-lock.json
generated
@@ -35,19 +35,18 @@
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
|
||||
19
Dashboard/package-lock.json
generated
19
Dashboard/package-lock.json
generated
@@ -40,19 +40,18 @@
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import AlertElement from "./Alert";
|
||||
import { Black } from "Common/Types/BrandColors";
|
||||
|
||||
165
Dashboard/src/Components/Exceptions/ExceptionInstanceTable.tsx
Normal file
165
Dashboard/src/Components/Exceptions/ExceptionInstanceTable.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
import AnalyticsModelTable from "Common/UI/Components/ModelTable/AnalyticsModelTable";
|
||||
import ExceptionInstance from "Common/Models/AnalyticsModels/ExceptionInstance";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import TraceElement from "../Traces/TraceElement";
|
||||
import SpanStatusElement from "../Span/SpanStatusElement";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
|
||||
export interface ComponentProps {
|
||||
title: string;
|
||||
description: string;
|
||||
query: Query<ExceptionInstance>;
|
||||
}
|
||||
|
||||
const ExceptionInstanceTable: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const computedQuery: Query<ExceptionInstance> = useMemo(() => {
|
||||
const query: Query<ExceptionInstance> = {
|
||||
...(props.query || {}),
|
||||
};
|
||||
|
||||
const projectId: ObjectID | null = ProjectUtil.getCurrentProjectId();
|
||||
|
||||
if (projectId && !query.projectId) {
|
||||
query.projectId = projectId;
|
||||
}
|
||||
|
||||
return query;
|
||||
}, [props.query]);
|
||||
|
||||
return (
|
||||
<AnalyticsModelTable<ExceptionInstance>
|
||||
modelType={ExceptionInstance}
|
||||
id="exception-instance-table"
|
||||
name="ExceptionInstance"
|
||||
singularName="Exception"
|
||||
pluralName="Exceptions"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
isViewable={false}
|
||||
userPreferencesKey="exception-instance-table"
|
||||
cardProps={{
|
||||
title: props.title,
|
||||
description: props.description,
|
||||
}}
|
||||
query={computedQuery}
|
||||
sortBy="time"
|
||||
sortOrder={SortOrder.Descending}
|
||||
noItemsMessage="No exception instances found."
|
||||
showRefreshButton={true}
|
||||
showViewIdButton={true}
|
||||
filters={[
|
||||
{
|
||||
field: {
|
||||
serviceId: true,
|
||||
},
|
||||
type: FieldType.Text,
|
||||
title: "Telemetry Service",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
exceptionType: true,
|
||||
},
|
||||
type: FieldType.Text,
|
||||
title: "Exception Type",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
time: true,
|
||||
},
|
||||
type: FieldType.DateTime,
|
||||
title: "Time",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
traceId: true,
|
||||
},
|
||||
type: FieldType.Text,
|
||||
title: "Trace ID",
|
||||
},
|
||||
]}
|
||||
selectMoreFields={{
|
||||
spanStatusCode: true,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
time: true,
|
||||
},
|
||||
title: "Time",
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
serviceId: true,
|
||||
},
|
||||
title: "Telemetry Service ID",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
exceptionType: true,
|
||||
},
|
||||
title: "Exception Type",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
message: true,
|
||||
},
|
||||
title: "Message",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
spanId: true,
|
||||
},
|
||||
title: "Span",
|
||||
type: FieldType.Element,
|
||||
getElement: (exceptionInstance: ExceptionInstance): ReactElement => {
|
||||
if (!exceptionInstance.spanId) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SpanStatusElement
|
||||
traceId={exceptionInstance.traceId?.toString()}
|
||||
spanStatusCode={exceptionInstance.spanStatusCode || 0}
|
||||
title={exceptionInstance.spanId?.toString()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
traceId: true,
|
||||
},
|
||||
title: "Trace",
|
||||
type: FieldType.Element,
|
||||
getElement: (exceptionInstance: ExceptionInstance): ReactElement => {
|
||||
if (!exceptionInstance.traceId) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TraceElement traceId={exceptionInstance.traceId.toString()} />
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExceptionInstanceTable;
|
||||
@@ -0,0 +1,296 @@
|
||||
import MonitorStepExceptionMonitor, {
|
||||
MonitorStepExceptionMonitorUtil,
|
||||
} from "Common/Types/Monitor/MonitorStepExceptionMonitor";
|
||||
import TelemetryService from "Common/Models/DatabaseModels/TelemetryService";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import BasicForm from "Common/UI/Components/Forms/BasicForm";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
import FieldLabelElement from "Common/UI/Components/Forms/Fields/FieldLabel";
|
||||
import HorizontalRule from "Common/UI/Components/HorizontalRule/HorizontalRule";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import JSONFunctions from "Common/Types/JSONFunctions";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
import ExceptionInstance from "Common/Models/AnalyticsModels/ExceptionInstance";
|
||||
import ExceptionInstanceTable from "../../../Exceptions/ExceptionInstanceTable";
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorStepExceptionMonitor: MonitorStepExceptionMonitor;
|
||||
onMonitorStepExceptionMonitorChanged: (
|
||||
monitorStepExceptionMonitor: MonitorStepExceptionMonitor,
|
||||
) => void;
|
||||
telemetryServices: Array<TelemetryService>;
|
||||
}
|
||||
|
||||
type ExceptionMonitorFormValues = {
|
||||
message: string;
|
||||
exceptionTypesInput: string;
|
||||
telemetryServiceIds: Array<string>;
|
||||
includeResolved: boolean;
|
||||
includeArchived: boolean;
|
||||
lastXSecondsOfExceptions: number;
|
||||
};
|
||||
|
||||
const DURATION_OPTIONS: Array<{ label: string; value: number }> = [
|
||||
{ label: "Last 5 seconds", value: 5 },
|
||||
{ label: "Last 10 seconds", value: 10 },
|
||||
{ label: "Last 30 seconds", value: 30 },
|
||||
{ label: "Last 1 minute", value: 60 },
|
||||
{ label: "Last 5 minutes", value: 300 },
|
||||
{ label: "Last 15 minutes", value: 900 },
|
||||
{ label: "Last 30 minutes", value: 1800 },
|
||||
{ label: "Last 1 hour", value: 3600 },
|
||||
{ label: "Last 6 hours", value: 21600 },
|
||||
{ label: "Last 12 hours", value: 43200 },
|
||||
{ label: "Last 24 hours", value: 86400 },
|
||||
];
|
||||
|
||||
type ParseExceptionTypesFunction = (input: string) => Array<string>;
|
||||
|
||||
const parseExceptionTypes: ParseExceptionTypesFunction = (input: string) => {
|
||||
return input
|
||||
.split(",")
|
||||
.map((item: string): string => {
|
||||
return item.trim();
|
||||
})
|
||||
.filter((item: string): boolean => {
|
||||
return item.length > 0;
|
||||
});
|
||||
};
|
||||
|
||||
type ToFormValuesFunction = (
|
||||
monitor: MonitorStepExceptionMonitor,
|
||||
) => ExceptionMonitorFormValues;
|
||||
|
||||
const toFormValues: ToFormValuesFunction = (
|
||||
monitor: MonitorStepExceptionMonitor,
|
||||
) => {
|
||||
return {
|
||||
message: monitor.message || "",
|
||||
exceptionTypesInput: monitor.exceptionTypes.join(", "),
|
||||
telemetryServiceIds: monitor.telemetryServiceIds.map(
|
||||
(id: ObjectID): string => {
|
||||
return id.toString();
|
||||
},
|
||||
),
|
||||
includeResolved: monitor.includeResolved || false,
|
||||
includeArchived: monitor.includeArchived || false,
|
||||
lastXSecondsOfExceptions:
|
||||
monitor.lastXSecondsOfExceptions ||
|
||||
MonitorStepExceptionMonitorUtil.getDefault().lastXSecondsOfExceptions,
|
||||
};
|
||||
};
|
||||
|
||||
type ToMonitorConfigFunction = (
|
||||
values: ExceptionMonitorFormValues,
|
||||
) => MonitorStepExceptionMonitor;
|
||||
|
||||
const toMonitorConfig: ToMonitorConfigFunction = (
|
||||
values: ExceptionMonitorFormValues,
|
||||
) => {
|
||||
return {
|
||||
telemetryServiceIds: values.telemetryServiceIds
|
||||
.filter((id: string): boolean => {
|
||||
return Boolean(id);
|
||||
})
|
||||
.map((id: string): ObjectID => {
|
||||
return new ObjectID(id);
|
||||
}),
|
||||
exceptionTypes: parseExceptionTypes(values.exceptionTypesInput),
|
||||
message: values.message || "",
|
||||
includeResolved: values.includeResolved || false,
|
||||
includeArchived: values.includeArchived || false,
|
||||
lastXSecondsOfExceptions:
|
||||
values.lastXSecondsOfExceptions ||
|
||||
MonitorStepExceptionMonitorUtil.getDefault().lastXSecondsOfExceptions,
|
||||
};
|
||||
};
|
||||
|
||||
type HasAdvancedConfigurationFunction = (
|
||||
monitor: MonitorStepExceptionMonitor,
|
||||
) => boolean;
|
||||
|
||||
const hasAdvancedConfiguration: HasAdvancedConfigurationFunction = (
|
||||
monitor: MonitorStepExceptionMonitor,
|
||||
) => {
|
||||
return (
|
||||
monitor.includeResolved ||
|
||||
monitor.includeArchived ||
|
||||
(monitor.telemetryServiceIds && monitor.telemetryServiceIds.length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
const ExceptionMonitorStepForm: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [formValues, setFormValues] = useState<ExceptionMonitorFormValues>(
|
||||
toFormValues(props.monitorStepExceptionMonitor),
|
||||
);
|
||||
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState<boolean>(
|
||||
hasAdvancedConfiguration(props.monitorStepExceptionMonitor),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFormValues(toFormValues(props.monitorStepExceptionMonitor));
|
||||
setShowAdvancedOptions(
|
||||
hasAdvancedConfiguration(props.monitorStepExceptionMonitor),
|
||||
);
|
||||
}, [props.monitorStepExceptionMonitor]);
|
||||
|
||||
type HandleFormChangeFunction = (values: ExceptionMonitorFormValues) => void;
|
||||
|
||||
const handleFormChange: HandleFormChangeFunction = (
|
||||
values: ExceptionMonitorFormValues,
|
||||
) => {
|
||||
setFormValues(values);
|
||||
props.onMonitorStepExceptionMonitorChanged(toMonitorConfig(values));
|
||||
};
|
||||
|
||||
const handleAdvancedToggle: () => void = (): void => {
|
||||
setShowAdvancedOptions((current: boolean): boolean => {
|
||||
return !current;
|
||||
});
|
||||
};
|
||||
|
||||
const previewQuery: Query<ExceptionInstance> = useMemo(() => {
|
||||
const monitorConfig: MonitorStepExceptionMonitor =
|
||||
toMonitorConfig(formValues);
|
||||
|
||||
return JSONFunctions.anyObjectToJSONObject(
|
||||
MonitorStepExceptionMonitorUtil.toAnalyticsQuery(monitorConfig),
|
||||
) as Query<ExceptionInstance>;
|
||||
}, [formValues]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BasicForm
|
||||
id="exception-monitor-form"
|
||||
hideSubmitButton={true}
|
||||
initialValues={formValues}
|
||||
onChange={handleFormChange}
|
||||
fields={[
|
||||
{
|
||||
field: {
|
||||
message: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
title: "Filter Exception Message",
|
||||
description:
|
||||
"Filter exceptions that include this text in the message.",
|
||||
hideOptionalLabel: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
exceptionTypesInput: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
title: "Exception Types",
|
||||
description:
|
||||
"Provide a comma-separated list of exception types to monitor.",
|
||||
placeholder: "TypeError, NullReferenceException",
|
||||
hideOptionalLabel: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
lastXSecondsOfExceptions: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownOptions: DURATION_OPTIONS,
|
||||
title: "Monitor exceptions for (time)",
|
||||
description:
|
||||
"We will evaluate exceptions generated within this time window.",
|
||||
defaultValue:
|
||||
MonitorStepExceptionMonitorUtil.getDefault()
|
||||
.lastXSecondsOfExceptions,
|
||||
hideOptionalLabel: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
telemetryServiceIds: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownOptions: props.telemetryServices.map(
|
||||
(service: TelemetryService): { label: string; value: string } => {
|
||||
return {
|
||||
label: service.name || "Untitled Service",
|
||||
value: service.id?.toString() || "",
|
||||
};
|
||||
},
|
||||
),
|
||||
title: "Filter by Telemetry Service",
|
||||
description: "Select telemetry services to scope this monitor.",
|
||||
hideOptionalLabel: true,
|
||||
showIf: (): boolean => {
|
||||
return showAdvancedOptions;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
includeResolved: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Checkbox,
|
||||
title: "Include Resolved Exceptions",
|
||||
description: "When enabled, resolved exceptions will be counted.",
|
||||
hideOptionalLabel: true,
|
||||
showIf: (): boolean => {
|
||||
return showAdvancedOptions;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
includeArchived: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Checkbox,
|
||||
title: "Include Archived Exceptions",
|
||||
description:
|
||||
"When enabled, archived exceptions will be included in results.",
|
||||
hideOptionalLabel: true,
|
||||
showIf: (): boolean => {
|
||||
return showAdvancedOptions;
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="-ml-3">
|
||||
<Button
|
||||
buttonStyle={ButtonStyleType.SECONDARY_LINK}
|
||||
title={
|
||||
showAdvancedOptions
|
||||
? "Hide Advanced Options"
|
||||
: "Show Advanced Options"
|
||||
}
|
||||
onClick={handleAdvancedToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<HorizontalRule />
|
||||
<FieldLabelElement
|
||||
title="Exceptions Preview"
|
||||
description={
|
||||
"Here is the preview of the exceptions that will be monitored based on the filters you have set above."
|
||||
}
|
||||
hideOptionalLabel={true}
|
||||
isHeading={true}
|
||||
/>
|
||||
<div className="mt-5 mb-5">
|
||||
<ExceptionInstanceTable
|
||||
title="Exceptions Preview"
|
||||
description="Exceptions matching the current monitor filters."
|
||||
query={previewQuery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExceptionMonitorStepForm;
|
||||
@@ -66,6 +66,10 @@ import MonitorStepMetricMonitor, {
|
||||
MonitorStepMetricMonitorUtil,
|
||||
} from "Common/Types/Monitor/MonitorStepMetricMonitor";
|
||||
import Link from "Common/UI/Components/Link/Link";
|
||||
import ExceptionMonitorStepForm from "./ExceptionMonitor/ExceptionMonitorStepForm";
|
||||
import MonitorStepExceptionMonitor, {
|
||||
MonitorStepExceptionMonitorUtil,
|
||||
} from "Common/Types/Monitor/MonitorStepExceptionMonitor";
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorStatusDropdownOptions: Array<DropdownOption>;
|
||||
@@ -648,6 +652,24 @@ return {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.monitorType === MonitorType.Exceptions && (
|
||||
<div className="mt-5">
|
||||
<ExceptionMonitorStepForm
|
||||
monitorStepExceptionMonitor={
|
||||
monitorStep.data?.exceptionMonitor ||
|
||||
MonitorStepExceptionMonitorUtil.getDefault()
|
||||
}
|
||||
telemetryServices={telemetryServices}
|
||||
onMonitorStepExceptionMonitorChanged={(
|
||||
value: MonitorStepExceptionMonitor,
|
||||
) => {
|
||||
monitorStep.setExceptionMonitor(value);
|
||||
props.onChange?.(MonitorStep.clone(monitorStep));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCodeMonitor && (
|
||||
<div className="mt-5">
|
||||
<FieldLabelElement
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../../Components/Monitor/Monitors";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import IncidentElement from "./Incident";
|
||||
|
||||
@@ -9,7 +9,7 @@ import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import ListResult from "Common/Types/BaseDatabase/ListResult";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
|
||||
import LabelsElement from "./Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import Exception from "Common/Types/Exception/Exception";
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Black } from "Common/Types/BrandColors";
|
||||
import Pill from "Common/UI/Components/Pill/Pill";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
label: Label;
|
||||
}
|
||||
|
||||
const LabelElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<Pill
|
||||
color={props.label.color || Black}
|
||||
text={props.label.name || ""}
|
||||
style={{
|
||||
marginRight: "5px",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelElement;
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorTypeUtil from "../../Utils/MonitorType";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
|
||||
|
||||
@@ -193,9 +193,7 @@ const SummaryInfo: FunctionComponent<ComponentProps> = (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{(props.monitorType === MonitorType.Logs ||
|
||||
props.monitorType === MonitorType.Traces ||
|
||||
props.monitorType === MonitorType.Metrics) && (
|
||||
{MonitorTypeHelper.isTelemetryMonitor(props.monitorType) && (
|
||||
<div className="space-y-6">
|
||||
<TelemetryMonitorSummaryView
|
||||
telemetryMonitorSummary={props.telemetryMonitorSummary}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import LabelsElement from "../Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../Monitor/Monitors";
|
||||
import StatusPagesElement from "../StatusPage/StatusPagesElement";
|
||||
import Route from "Common/Types/API/Route";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
|
||||
@@ -16,7 +16,7 @@ import IncidentSeverityElement from "../../IncidentSeverity/IncidentSeverityElem
|
||||
import IncidentStateElement from "../../IncidentState/IncidentStateElement";
|
||||
import ScheduledMaintenanceStateElement from "../../ScheduledMaintenanceState/ScheduledMaintenanceStateElement";
|
||||
import MonitorStatusElement from "../../MonitorStatus/MonitorStatusElement";
|
||||
import LabelElement from "../../Label/Label";
|
||||
import LabelElement from "Common/UI/Components/Label/Label";
|
||||
import MonitorElement from "../../Monitor/Monitor";
|
||||
import { GetReactElementFunction } from "Common/UI/Types/FunctionTypes";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../../PageComponentProps";
|
||||
import CodeRepositoryType from "Common/Types/CodeRepository/CodeRepositoryType";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Banner from "Common/UI/Components/Banner/Banner";
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ChangeAlertState from "../../../Components/Alert/ChangeState";
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import OnCallDutyPoliciesView from "../../../Components/OnCallPolicy/OnCallPolicies";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
@@ -49,6 +49,11 @@ import HeaderAlert, {
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import ColorSwatch from "Common/Types/ColorSwatch";
|
||||
import AlertFeedElement from "../../../Components/Alert/AlertFeed";
|
||||
import ExceptionInstanceTable from "../../../Components/Exceptions/ExceptionInstanceTable";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
import ExceptionInstance from "Common/Models/AnalyticsModels/ExceptionInstance";
|
||||
import Span from "Common/Models/AnalyticsModels/Span";
|
||||
import Log from "Common/Models/AnalyticsModels/Log";
|
||||
|
||||
const AlertView: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
@@ -526,7 +531,7 @@ const AlertView: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
<Card title={"Logs"} description={"Logs for this alert."}>
|
||||
<DashboardLogsViewer
|
||||
id="logs-preview"
|
||||
logQuery={telemetryQuery.telemetryQuery}
|
||||
logQuery={telemetryQuery.telemetryQuery as Query<Log>}
|
||||
limit={10}
|
||||
noLogsMessage="No logs found"
|
||||
/>
|
||||
@@ -538,7 +543,9 @@ const AlertView: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
telemetryQuery.telemetryType === TelemetryType.Trace &&
|
||||
telemetryQuery.telemetryQuery && (
|
||||
<div>
|
||||
<TraceTable spanQuery={telemetryQuery.telemetryQuery} />
|
||||
<TraceTable
|
||||
spanQuery={telemetryQuery.telemetryQuery as Query<Span>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -578,6 +585,16 @@ const AlertView: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{telemetryQuery &&
|
||||
telemetryQuery.telemetryType === TelemetryType.Exception &&
|
||||
telemetryQuery.telemetryQuery && (
|
||||
<ExceptionInstanceTable
|
||||
title="Exceptions"
|
||||
description="Exceptions for this alert."
|
||||
query={telemetryQuery.telemetryQuery as Query<ExceptionInstance>}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AlertFeedElement alertId={modelId} />
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
||||
@@ -45,6 +45,8 @@ import FetchMonitors from "../../Components/Monitor/FetchMonitors";
|
||||
import FetchIncidentSeverities from "../../Components/IncidentSeverity/FetchIncidentSeverity";
|
||||
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import Color from "Common/Types/Color";
|
||||
import { DropdownOption } from "Common/UI/Components/Dropdown/Dropdown";
|
||||
|
||||
const IncidentCreate: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -313,18 +315,24 @@ const IncidentCreate: FunctionComponent<
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
sort: {
|
||||
order: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
return incidentStates.data.map((state: IncidentState) => {
|
||||
return {
|
||||
label: state.name || "",
|
||||
value: state._id?.toString() || "",
|
||||
};
|
||||
});
|
||||
return incidentStates.data.map(
|
||||
(state: IncidentState): DropdownOption => {
|
||||
const option: DropdownOption = {
|
||||
label: state.name || "",
|
||||
value: state._id?.toString() || "",
|
||||
color: state.color as Color,
|
||||
};
|
||||
|
||||
return option;
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
// Silently fail and return empty array
|
||||
return [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ChangeIncidentState from "../../../Components/Incident/ChangeState";
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../../../Components/Monitor/Monitors";
|
||||
import OnCallDutyPoliciesView from "../../../Components/OnCallPolicy/OnCallPolicies";
|
||||
import SubscriberNotificationStatus from "../../../Components/StatusPageSubscribers/SubscriberNotificationStatus";
|
||||
@@ -53,6 +53,11 @@ import IncidentFeedElement from "../../../Components/Incident/IncidentFeed";
|
||||
import Monitor from "Common/Models/DatabaseModels/Monitor";
|
||||
import MonitorStatus from "Common/Models/DatabaseModels/MonitorStatus";
|
||||
import StatusPageSubscriberNotificationStatus from "Common/Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import ExceptionInstanceTable from "../../../Components/Exceptions/ExceptionInstanceTable";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
import Span from "Common/Models/AnalyticsModels/Span";
|
||||
import Log from "Common/Models/AnalyticsModels/Log";
|
||||
import ExceptionInstance from "Common/Models/AnalyticsModels/ExceptionInstance";
|
||||
|
||||
const IncidentView: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -620,7 +625,7 @@ const IncidentView: FunctionComponent<
|
||||
<Card title={"Logs"} description={"Logs for this incident."}>
|
||||
<DashboardLogsViewer
|
||||
id="logs-preview"
|
||||
logQuery={telemetryQuery.telemetryQuery}
|
||||
logQuery={telemetryQuery.telemetryQuery as Query<Log>}
|
||||
limit={10}
|
||||
noLogsMessage="No logs found"
|
||||
/>
|
||||
@@ -632,7 +637,9 @@ const IncidentView: FunctionComponent<
|
||||
telemetryQuery.telemetryType === TelemetryType.Trace &&
|
||||
telemetryQuery.telemetryQuery && (
|
||||
<div>
|
||||
<TraceTable spanQuery={telemetryQuery.telemetryQuery} />
|
||||
<TraceTable
|
||||
spanQuery={telemetryQuery.telemetryQuery as Query<Span>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -672,6 +679,16 @@ const IncidentView: FunctionComponent<
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{telemetryQuery &&
|
||||
telemetryQuery.telemetryType === TelemetryType.Exception &&
|
||||
telemetryQuery.telemetryQuery && (
|
||||
<ExceptionInstanceTable
|
||||
title="Exceptions"
|
||||
description="Exceptions related to this incident."
|
||||
query={telemetryQuery.telemetryQuery as Query<ExceptionInstance>}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IncidentFeedElement incidentId={modelId} />
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import DisabledWarning from "../../../Components/Monitor/DisabledWarning";
|
||||
import IncomingMonitorLink from "../../../Components/Monitor/IncomingRequestMonitor/IncomingMonitorLink";
|
||||
import ServerMonitorDocumentation from "../../../Components/Monitor/ServerMonitor/Documentation";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import CurrentStatusElement from "../../Components/MonitorGroup/CurrentStatus";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user