mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
7 Commits
postmortem
...
revoke-tok
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a445eabd9 | ||
|
|
645ebdbb4a | ||
|
|
3df2152737 | ||
|
|
65758b9eae | ||
|
|
64b4a309d4 | ||
|
|
e833e07a01 | ||
|
|
8844a5e302 |
@@ -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";
|
||||
@@ -152,6 +153,7 @@ import ServiceCatalogTelemetryService from "./ServiceCatalogTelemetryService";
|
||||
|
||||
import UserTotpAuth from "./UserTotpAuth";
|
||||
import UserWebAuthn from "./UserWebAuthn";
|
||||
import UserSession from "./UserSession";
|
||||
|
||||
import TelemetryIngestionKey from "./TelemetryIngestionKey";
|
||||
|
||||
@@ -266,6 +268,7 @@ const AllModelTypes: Array<{
|
||||
StatusPageFooterLink,
|
||||
StatusPageHeaderLink,
|
||||
StatusPagePrivateUser,
|
||||
StatusPagePrivateUserSession,
|
||||
StatusPageHistoryChartBarColorRule,
|
||||
|
||||
ScheduledMaintenanceState,
|
||||
@@ -318,6 +321,7 @@ const AllModelTypes: Array<{
|
||||
UserOnCallLog,
|
||||
UserOnCallLogTimeline,
|
||||
UserNotificationSetting,
|
||||
UserSession,
|
||||
|
||||
DataMigration,
|
||||
|
||||
|
||||
278
Common/Models/DatabaseModels/StatusPagePrivateUserSession.ts
Normal file
278
Common/Models/DatabaseModels/StatusPagePrivateUserSession.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Project from "./Project";
|
||||
import StatusPage from "./StatusPage";
|
||||
import StatusPagePrivateUser from "./StatusPagePrivateUser";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
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 IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation({
|
||||
isMasterAdminApiDocs: true,
|
||||
})
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@Entity({
|
||||
name: "StatusPagePrivateUserSession",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "StatusPagePrivateUserSession",
|
||||
singularName: "Status Page Session",
|
||||
pluralName: "Status Page Sessions",
|
||||
icon: IconProp.Clock,
|
||||
tableDescription:
|
||||
"Stores refresh tokens and metadata for authenticated status page sessions.",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("statusPagePrivateUserId")
|
||||
class StatusPagePrivateUserSession extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPagePrivateUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Private User",
|
||||
description: "Status page private user associated with this session.",
|
||||
modelType: StatusPagePrivateUser,
|
||||
})
|
||||
@ManyToOne(() => {
|
||||
return StatusPagePrivateUser;
|
||||
})
|
||||
@JoinColumn({ name: "statusPagePrivateUserId" })
|
||||
public statusPagePrivateUser?: StatusPagePrivateUser = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Private User ID",
|
||||
description: "ID of the status page private user.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPagePrivateUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Status Page",
|
||||
description: "Status page for which this session is valid.",
|
||||
modelType: StatusPage,
|
||||
})
|
||||
@ManyToOne(() => {
|
||||
return StatusPage;
|
||||
})
|
||||
@JoinColumn({ name: "statusPageId" })
|
||||
public statusPage?: StatusPage = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Status Page ID",
|
||||
description: "Identifier of the status page tied to this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Project",
|
||||
description: "Project that owns the status page.",
|
||||
modelType: Project,
|
||||
})
|
||||
@ManyToOne(() => {
|
||||
return Project;
|
||||
})
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Project ID",
|
||||
description: "Project identifier for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.LongText,
|
||||
title: "Refresh Token",
|
||||
description: "Refresh token for this session.",
|
||||
hashed: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
public refreshToken?: string = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Expires At",
|
||||
description: "When this refresh token expires.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
default: () => {
|
||||
return "now()";
|
||||
},
|
||||
})
|
||||
public refreshTokenExpiresAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Last Used At",
|
||||
description: "When this session was last used.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
default: () => {
|
||||
return "now()";
|
||||
},
|
||||
})
|
||||
public lastUsedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "IP Address",
|
||||
description: "IP address associated with this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public ipAddress?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.LongText,
|
||||
title: "User Agent",
|
||||
description: "User agent information for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
nullable: true,
|
||||
})
|
||||
public userAgent?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device",
|
||||
description: "Device description captured for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public device?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Revoked",
|
||||
description: "Marks whether this session has been revoked.",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isRevoked?: boolean = undefined;
|
||||
}
|
||||
|
||||
export default StatusPagePrivateUserSession;
|
||||
267
Common/Models/DatabaseModels/UserSession.ts
Normal file
267
Common/Models/DatabaseModels/UserSession.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Project from "./Project";
|
||||
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 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 IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
|
||||
@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.Clock,
|
||||
tableDescription:
|
||||
"Stores refresh tokens and metadata for authenticated dashboard sessions.",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserSession extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "User",
|
||||
description: "User associated with this session.",
|
||||
modelType: User,
|
||||
})
|
||||
@ManyToOne(() => {
|
||||
return User;
|
||||
})
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "User ID",
|
||||
description: "ID of the user associated with this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.LongText,
|
||||
title: "Refresh",
|
||||
description: "Refresh token for this session.",
|
||||
hashed: true,
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
public refreshToken?: string = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Expires At",
|
||||
description: "When this refresh token expires.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
default: () => {
|
||||
return "now()";
|
||||
},
|
||||
})
|
||||
public refreshTokenExpiresAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Last Used At",
|
||||
description: "When this session was last used.",
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
default: () => {
|
||||
return "now()";
|
||||
},
|
||||
})
|
||||
public lastUsedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "IP Address",
|
||||
description: "IP address used when this session was created or refreshed.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public ipAddress?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.LongText,
|
||||
title: "User Agent",
|
||||
description: "User agent string associated with this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
nullable: true,
|
||||
})
|
||||
public userAgent?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device",
|
||||
description: "Device description associated with this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public device?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Revoked",
|
||||
description: "Marks whether this session has been revoked.",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isRevoked?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Global Login",
|
||||
description:
|
||||
"Indicates if this session was created via global login (non-SSO).",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: true,
|
||||
})
|
||||
public isGlobalLogin?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Project",
|
||||
description: "Project tied to this session when authenticated via SSO.",
|
||||
modelType: Project,
|
||||
})
|
||||
@ManyToOne(() => {
|
||||
return Project;
|
||||
})
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Project ID",
|
||||
description: "Project identifier for SSO sessions.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
}
|
||||
|
||||
export default UserSession;
|
||||
@@ -1,14 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1762554602716 implements MigrationInterface {
|
||||
public name = 'MigrationName1762554602716'
|
||||
public name = "MigrationName1762554602716";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "jwtRefreshToken"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "User" ADD "jwtRefreshToken" character varying(100)`);
|
||||
}
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "jwtRefreshToken"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "User" ADD "jwtRefreshToken" character varying(100)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1762776676073 implements MigrationInterface {
|
||||
public name = "MigrationName1762776676073";
|
||||
|
||||
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, "statusPagePrivateUserId" uuid NOT NULL, "statusPageId" uuid NOT NULL, "projectId" uuid NOT NULL, "refreshToken" character varying(500) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "ipAddress" character varying(100), "userAgent" character varying, "device" character varying(100), "isRevoked" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_12ce827a16d121bf6719260b8a9" UNIQUE ("refreshToken"), CONSTRAINT "PK_cbace84fe4c9712b94e571dc133" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_365d602943505272f8f651ff4e" ON "StatusPagePrivateUserSession" ("statusPagePrivateUserId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7b8d9b6e068c045d56b47a484b" ON "StatusPagePrivateUserSession" ("statusPageId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_12ce827a16d121bf6719260b8a" ON "StatusPagePrivateUserSession" ("refreshToken") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b317c0aa45b418b14515c31dff" ON "StatusPagePrivateUserSession" ("refreshTokenExpiresAt") `,
|
||||
);
|
||||
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(500) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "ipAddress" character varying(100), "userAgent" character varying, "device" character varying(100), "isRevoked" boolean NOT NULL DEFAULT false, "isGlobalLogin" boolean NOT NULL DEFAULT true, "projectId" uuid, 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 INDEX "IDX_d66bd8342b0005c7192bdb17ef" ON "UserSession" ("refreshToken") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f94389c48308f3d4f7258c24ed" ON "UserSession" ("refreshTokenExpiresAt") `,
|
||||
);
|
||||
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_365d602943505272f8f651ff4e8" FOREIGN KEY ("statusPagePrivateUserId") REFERENCES "StatusPagePrivateUser"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" ADD CONSTRAINT "FK_7353eaf92987aeaf38c2590e943" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" ADD CONSTRAINT "FK_bb37d85f31bf216476fa5f93d9c" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" DROP CONSTRAINT "FK_bb37d85f31bf216476fa5f93d9c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" DROP CONSTRAINT "FK_7353eaf92987aeaf38c2590e943"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_365d602943505272f8f651ff4e8"`,
|
||||
);
|
||||
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_f94389c48308f3d4f7258c24ed"`,
|
||||
);
|
||||
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_b317c0aa45b418b14515c31dff"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_12ce827a16d121bf6719260b8a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7b8d9b6e068c045d56b47a484b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_365d602943505272f8f651ff4e"`,
|
||||
);
|
||||
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 { MigrationName1762776676073 } from "./1762776676073-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -365,5 +366,6 @@ export default [
|
||||
MigrationName1761232578396,
|
||||
MigrationName1761834523183,
|
||||
MigrationName1762181014879,
|
||||
MigrationName1762554602716
|
||||
MigrationName1762554602716,
|
||||
MigrationName1762776676073,
|
||||
];
|
||||
|
||||
292
Common/Server/Services/UserSessionService.ts
Normal file
292
Common/Server/Services/UserSessionService.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import UserSession from "../../Models/DatabaseModels/UserSession";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import JSONWebToken from "../Utils/JsonWebToken";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import {
|
||||
ACCESS_TOKEN_TTL_SECONDS,
|
||||
GeneratedAuthTokens,
|
||||
SessionMetadata,
|
||||
computeRefreshTokenExpiryDate,
|
||||
generateRefreshToken,
|
||||
getAccessTokenExpiresInSeconds,
|
||||
getRefreshTokenExpiresInSeconds,
|
||||
hashRefreshToken,
|
||||
} from "../Utils/SessionToken";
|
||||
import QueryDeepPartialEntity from "../../Types/Database/PartialEntity";
|
||||
|
||||
export type UserSessionWithTokens = GeneratedAuthTokens & {
|
||||
session: UserSession;
|
||||
};
|
||||
|
||||
export class Service extends DatabaseService<UserSession> {
|
||||
public constructor() {
|
||||
super(UserSession);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async createSessionWithTokens(data: {
|
||||
user: User;
|
||||
isGlobalLogin: boolean;
|
||||
projectId?: ObjectID;
|
||||
metadata?: SessionMetadata;
|
||||
props?: DatabaseCommonInteractionProps;
|
||||
}): Promise<UserSessionWithTokens> {
|
||||
const { user, isGlobalLogin, projectId, metadata, props } = data;
|
||||
|
||||
if (!user.id) {
|
||||
throw new Error("User must have an id to create a session.");
|
||||
}
|
||||
|
||||
const now: Date = OneUptimeDate.getCurrentDate();
|
||||
const refreshToken: string = generateRefreshToken();
|
||||
const refreshTokenHash: string = hashRefreshToken(refreshToken);
|
||||
const refreshTokenExpiresAt: Date = computeRefreshTokenExpiryDate();
|
||||
|
||||
const session: UserSession = new UserSession();
|
||||
session.userId = user.id;
|
||||
session.isGlobalLogin = isGlobalLogin;
|
||||
|
||||
if (projectId) {
|
||||
session.projectId = projectId;
|
||||
}
|
||||
|
||||
session.refreshToken = new HashedString(refreshTokenHash, true);
|
||||
session.refreshTokenExpiresAt = refreshTokenExpiresAt;
|
||||
session.lastUsedAt = now;
|
||||
if (metadata?.ipAddress !== undefined) {
|
||||
session.ipAddress = metadata.ipAddress;
|
||||
}
|
||||
|
||||
if (metadata?.userAgent !== undefined) {
|
||||
session.userAgent = metadata.userAgent;
|
||||
}
|
||||
|
||||
if (metadata?.device !== undefined) {
|
||||
session.device = metadata.device;
|
||||
}
|
||||
|
||||
const createdSession: UserSession = await this.create({
|
||||
data: session,
|
||||
props: props || {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken: string = this.createAccessToken({
|
||||
user,
|
||||
isGlobalLogin,
|
||||
...(projectId ? { projectId } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accessTokenExpiresInSeconds: getAccessTokenExpiresInSeconds(),
|
||||
refreshTokenExpiresInSeconds: getRefreshTokenExpiresInSeconds(),
|
||||
refreshTokenExpiresAt,
|
||||
session: createdSession,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async rotateSessionTokens(data: {
|
||||
session: UserSession;
|
||||
user: User;
|
||||
metadata?: SessionMetadata;
|
||||
props?: DatabaseCommonInteractionProps;
|
||||
}): Promise<UserSessionWithTokens> {
|
||||
const { session, user, metadata, props } = data;
|
||||
|
||||
if (!session.id) {
|
||||
throw new Error("Session id is required to rotate tokens.");
|
||||
}
|
||||
|
||||
const refreshToken: string = generateRefreshToken();
|
||||
const refreshTokenHash: string = hashRefreshToken(refreshToken);
|
||||
const refreshTokenExpiresAt: Date = computeRefreshTokenExpiryDate();
|
||||
const now: Date = OneUptimeDate.getCurrentDate();
|
||||
|
||||
session.refreshToken = new HashedString(refreshTokenHash, true);
|
||||
session.refreshTokenExpiresAt = refreshTokenExpiresAt;
|
||||
session.lastUsedAt = now;
|
||||
if (metadata?.ipAddress !== undefined) {
|
||||
session.ipAddress = metadata.ipAddress;
|
||||
}
|
||||
|
||||
if (metadata?.userAgent !== undefined) {
|
||||
session.userAgent = metadata.userAgent;
|
||||
}
|
||||
|
||||
if (metadata?.device !== undefined) {
|
||||
session.device = metadata.device;
|
||||
}
|
||||
session.isRevoked = false;
|
||||
|
||||
const updateData: QueryDeepPartialEntity<UserSession> = {
|
||||
refreshToken: session.refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
lastUsedAt: now,
|
||||
isRevoked: false,
|
||||
};
|
||||
|
||||
if (metadata?.ipAddress !== undefined) {
|
||||
updateData.ipAddress = metadata.ipAddress;
|
||||
}
|
||||
|
||||
if (metadata?.userAgent !== undefined) {
|
||||
updateData.userAgent = metadata.userAgent;
|
||||
}
|
||||
|
||||
if (metadata?.device !== undefined) {
|
||||
updateData.device = metadata.device;
|
||||
}
|
||||
|
||||
await this.updateOneById({
|
||||
id: session.id,
|
||||
data: updateData,
|
||||
props: props || {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken: string = this.createAccessToken({
|
||||
user,
|
||||
isGlobalLogin: Boolean(session.isGlobalLogin),
|
||||
...(session.projectId ? { projectId: session.projectId } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accessTokenExpiresInSeconds: getAccessTokenExpiresInSeconds(),
|
||||
refreshTokenExpiresInSeconds: getRefreshTokenExpiresInSeconds(),
|
||||
refreshTokenExpiresAt,
|
||||
session,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async getActiveSessionByRefreshToken(data: {
|
||||
refreshToken: string;
|
||||
props?: DatabaseCommonInteractionProps;
|
||||
}): Promise<UserSession | null> {
|
||||
const { refreshToken, props } = data;
|
||||
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const refreshTokenHash: string = hashRefreshToken(refreshToken);
|
||||
|
||||
const session: UserSession | null = await this.findOneBy({
|
||||
query: {
|
||||
refreshToken: new HashedString(refreshTokenHash, true),
|
||||
isRevoked: false,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
isGlobalLogin: true,
|
||||
projectId: true,
|
||||
refreshTokenExpiresAt: true,
|
||||
},
|
||||
props: props || {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (session.refreshTokenExpiresAt) {
|
||||
const refreshExpired: boolean = OneUptimeDate.hasExpired(
|
||||
session.refreshTokenExpiresAt,
|
||||
);
|
||||
|
||||
if (refreshExpired) {
|
||||
await this.revokeSessionById({
|
||||
sessionId: session.id!,
|
||||
...(props ? { props } : {}),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async revokeSessionByRefreshToken(data: {
|
||||
refreshToken: string;
|
||||
props?: DatabaseCommonInteractionProps;
|
||||
}): Promise<void> {
|
||||
const { refreshToken, props } = data;
|
||||
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshTokenHash: string = hashRefreshToken(refreshToken);
|
||||
|
||||
await this.updateOneBy({
|
||||
query: {
|
||||
refreshToken: new HashedString(refreshTokenHash, true),
|
||||
},
|
||||
data: {
|
||||
isRevoked: true,
|
||||
refreshTokenExpiresAt: OneUptimeDate.getCurrentDate(),
|
||||
},
|
||||
props: props || {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async revokeSessionById(data: {
|
||||
sessionId: ObjectID;
|
||||
props?: DatabaseCommonInteractionProps;
|
||||
}): Promise<void> {
|
||||
const { sessionId, props } = data;
|
||||
|
||||
await this.updateOneById({
|
||||
id: sessionId,
|
||||
data: {
|
||||
isRevoked: true,
|
||||
refreshTokenExpiresAt: OneUptimeDate.getCurrentDate(),
|
||||
},
|
||||
props: props || {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private createAccessToken(data: {
|
||||
user: User;
|
||||
isGlobalLogin: boolean;
|
||||
projectId?: ObjectID;
|
||||
}): string {
|
||||
const { user, isGlobalLogin, projectId } = data;
|
||||
|
||||
return JSONWebToken.sign({
|
||||
data: {
|
||||
userId: user.id!,
|
||||
email: user.email!,
|
||||
name: user.name!,
|
||||
timezone: user.timezone || null,
|
||||
isMasterAdmin: Boolean(user.isMasterAdmin),
|
||||
isGlobalLogin,
|
||||
projectId: projectId || undefined,
|
||||
},
|
||||
expiresInSeconds: ACCESS_TOKEN_TTL_SECONDS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -3,6 +3,7 @@ import Dictionary from "../../Types/Dictionary";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { CookieOptions } from "express";
|
||||
import JSONWebToken from "./JsonWebToken";
|
||||
import StatusPagePrivateUser from "../../Models/DatabaseModels/StatusPagePrivateUser";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import PositiveNumber from "../../Types/PositiveNumber";
|
||||
@@ -57,92 +58,186 @@ export default class CookieUtil {
|
||||
public static setUserCookie(data: {
|
||||
expressResponse: ExpressResponse;
|
||||
user: User;
|
||||
isGlobalLogin: boolean;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessTokenExpiresInSeconds: number;
|
||||
refreshTokenExpiresInSeconds: number;
|
||||
}): void {
|
||||
const { expressResponse: res, user, isGlobalLogin } = data;
|
||||
const {
|
||||
expressResponse: res,
|
||||
user,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accessTokenExpiresInSeconds,
|
||||
refreshTokenExpiresInSeconds,
|
||||
} = data;
|
||||
|
||||
const token: string = JSONWebToken.signUserLoginToken({
|
||||
tokenData: {
|
||||
userId: user.id!,
|
||||
email: user.email!,
|
||||
name: user.name!,
|
||||
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.
|
||||
},
|
||||
expiresInSeconds: OneUptimeDate.getSecondsInDays(new PositiveNumber(30)),
|
||||
});
|
||||
const accessTokenMaxAge: number = Math.max(
|
||||
0,
|
||||
Math.round(accessTokenExpiresInSeconds * 1000),
|
||||
);
|
||||
const refreshTokenMaxAge: number = Math.max(
|
||||
0,
|
||||
Math.round(refreshTokenExpiresInSeconds * 1000),
|
||||
);
|
||||
|
||||
// Set a cookie with token.
|
||||
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), token, {
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), accessToken, {
|
||||
maxAge: accessTokenMaxAge,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserRefreshTokenKey(),
|
||||
refreshToken,
|
||||
{
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
},
|
||||
);
|
||||
|
||||
if (user.id) {
|
||||
// set user id cookie
|
||||
CookieUtil.setCookie(res, CookieName.UserID, user.id!.toString(), {
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: false,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
if (user.email) {
|
||||
// set user email cookie
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieName.Email,
|
||||
user.email?.toString() || "",
|
||||
{
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: false,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (user.name) {
|
||||
// set user name cookie
|
||||
CookieUtil.setCookie(res, CookieName.Name, user.name?.toString() || "", {
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: false,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
if (user.timezone) {
|
||||
// set user timezone cookie
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieName.Timezone,
|
||||
user.timezone?.toString() || "",
|
||||
{
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: false,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (user.isMasterAdmin) {
|
||||
// set user isMasterAdmin cookie
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieName.IsMasterAdmin,
|
||||
user.isMasterAdmin?.toString() || "",
|
||||
{
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: false,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (user.profilePictureId) {
|
||||
// set user profile picture id cookie
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieName.ProfilePicID,
|
||||
user.profilePictureId?.toString() || "",
|
||||
{
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: false,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static setStatusPagePrivateUserCookie(data: {
|
||||
expressResponse: ExpressResponse;
|
||||
user: StatusPagePrivateUser;
|
||||
statusPageId: ObjectID;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessTokenExpiresInSeconds: number;
|
||||
refreshTokenExpiresInSeconds: number;
|
||||
}): void {
|
||||
const {
|
||||
expressResponse: res,
|
||||
user,
|
||||
statusPageId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accessTokenExpiresInSeconds,
|
||||
refreshTokenExpiresInSeconds,
|
||||
} = data;
|
||||
|
||||
const accessTokenMaxAge: number = Math.max(
|
||||
0,
|
||||
Math.round(accessTokenExpiresInSeconds * 1000),
|
||||
);
|
||||
const refreshTokenMaxAge: number = Math.max(
|
||||
0,
|
||||
Math.round(refreshTokenExpiresInSeconds * 1000),
|
||||
);
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserTokenKey(statusPageId),
|
||||
accessToken,
|
||||
{
|
||||
maxAge: accessTokenMaxAge,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserRefreshTokenKey(statusPageId),
|
||||
refreshToken,
|
||||
{
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
},
|
||||
);
|
||||
|
||||
if (user.email) {
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
`${CookieName.Email}-${statusPageId.toString()}`,
|
||||
user.email?.toString() || "",
|
||||
{
|
||||
maxAge: refreshTokenMaxAge,
|
||||
httpOnly: false,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -190,6 +285,15 @@ export default class CookieUtil {
|
||||
return `${CookieName.Token}-${id.toString()}`;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getUserRefreshTokenKey(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()}`;
|
||||
@@ -211,4 +315,44 @@ export default class CookieUtil {
|
||||
this.removeCookie(res, key);
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getUserRefreshTokenFromExpressRequest(
|
||||
req: ExpressRequest,
|
||||
): string | undefined {
|
||||
return this.getCookieFromExpressRequest(req, this.getUserRefreshTokenKey());
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getStatusPageRefreshTokenFromExpressRequest(
|
||||
req: ExpressRequest,
|
||||
statusPageId: ObjectID,
|
||||
): string | undefined {
|
||||
return this.getCookieFromExpressRequest(
|
||||
req,
|
||||
this.getUserRefreshTokenKey(statusPageId),
|
||||
);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static removeUserAuthCookies(res: ExpressResponse): void {
|
||||
this.removeCookie(res, this.getUserTokenKey());
|
||||
this.removeCookie(res, this.getUserRefreshTokenKey());
|
||||
this.removeCookie(res, CookieName.UserID);
|
||||
this.removeCookie(res, CookieName.Email);
|
||||
this.removeCookie(res, CookieName.Name);
|
||||
this.removeCookie(res, CookieName.Timezone);
|
||||
this.removeCookie(res, CookieName.IsMasterAdmin);
|
||||
this.removeCookie(res, CookieName.ProfilePicID);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static removeStatusPageAuthCookies(
|
||||
res: ExpressResponse,
|
||||
statusPageId: ObjectID,
|
||||
): void {
|
||||
this.removeCookie(res, this.getUserTokenKey(statusPageId));
|
||||
this.removeCookie(res, this.getUserRefreshTokenKey(statusPageId));
|
||||
this.removeCookie(res, `${CookieName.Email}-${statusPageId.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
56
Common/Server/Utils/RequestMetadata.ts
Normal file
56
Common/Server/Utils/RequestMetadata.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { ExpressRequest } from "./Express";
|
||||
|
||||
export default class RequestMetadata {
|
||||
public static getClientIp(req: ExpressRequest): string | undefined {
|
||||
const forwardedFor: string | undefined =
|
||||
(req.headers["x-forwarded-for"] as string | undefined) || undefined;
|
||||
const realIp: string | undefined =
|
||||
(req.headers["x-real-ip"] as string | undefined) || undefined;
|
||||
const socketAddress: string | undefined =
|
||||
req.socket?.remoteAddress || undefined;
|
||||
const reqIp: string | undefined = req.ip;
|
||||
const reqIpsFirst: string | undefined = Array.isArray(req.ips)
|
||||
? req.ips[0]
|
||||
: undefined;
|
||||
|
||||
const candidate: Array<string | undefined> = [
|
||||
forwardedFor,
|
||||
realIp,
|
||||
socketAddress,
|
||||
reqIp,
|
||||
reqIpsFirst,
|
||||
];
|
||||
|
||||
for (const value of candidate) {
|
||||
if (value && value.trim()) {
|
||||
return value.split(",")[0]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public static getUserAgent(req: ExpressRequest): string | undefined {
|
||||
const userAgent: string | undefined = req.headers["user-agent"] as
|
||||
| string
|
||||
| undefined;
|
||||
return userAgent?.trim() || undefined;
|
||||
}
|
||||
|
||||
public static getDevice(req: ExpressRequest): string | undefined {
|
||||
const deviceHeader: string | undefined =
|
||||
(req.headers["x-device"] as string | undefined) || undefined;
|
||||
|
||||
if (deviceHeader && deviceHeader.trim()) {
|
||||
return deviceHeader.trim();
|
||||
}
|
||||
|
||||
const userAgent: string | undefined = this.getUserAgent(req);
|
||||
|
||||
if (userAgent) {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
42
Common/Server/Utils/SessionToken.ts
Normal file
42
Common/Server/Utils/SessionToken.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import { createHash, randomBytes } from "crypto";
|
||||
|
||||
export const ACCESS_TOKEN_TTL_SECONDS: number = 15 * 60; // 15 minutes
|
||||
export const REFRESH_TOKEN_TTL_SECONDS: number = 30 * 24 * 60 * 60; // 30 days
|
||||
|
||||
export type SessionMetadata = {
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
device?: string;
|
||||
};
|
||||
|
||||
export type GeneratedAuthTokens = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessTokenExpiresInSeconds: number;
|
||||
refreshTokenExpiresInSeconds: number;
|
||||
refreshTokenExpiresAt: Date;
|
||||
};
|
||||
|
||||
export const generateRefreshToken = (): string => {
|
||||
return randomBytes(48).toString("hex");
|
||||
};
|
||||
|
||||
export const hashRefreshToken = (token: string): string => {
|
||||
return createHash("sha256").update(token).digest("hex");
|
||||
};
|
||||
|
||||
export const computeRefreshTokenExpiryDate = (): Date => {
|
||||
return OneUptimeDate.addRemoveSeconds(
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
REFRESH_TOKEN_TTL_SECONDS,
|
||||
);
|
||||
};
|
||||
|
||||
export const getRefreshTokenExpiresInSeconds = (): number => {
|
||||
return REFRESH_TOKEN_TTL_SECONDS;
|
||||
};
|
||||
|
||||
export const getAccessTokenExpiresInSeconds = (): number => {
|
||||
return ACCESS_TOKEN_TTL_SECONDS;
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -75,8 +75,7 @@ export const WORKFLOW_HOSTNAME: Hostname = Hostname.fromString(HOST);
|
||||
|
||||
export const PROBE_INGEST_HOSTNAME: Hostname = Hostname.fromString(HOST);
|
||||
|
||||
export const TELEMETRY_HOSTNAME: Hostname =
|
||||
Hostname.fromString(HOST);
|
||||
export const TELEMETRY_HOSTNAME: Hostname = Hostname.fromString(HOST);
|
||||
|
||||
export const INCOMING_REQUEST_INGEST_HOSTNAME: Hostname =
|
||||
Hostname.fromString(HOST);
|
||||
|
||||
@@ -60,7 +60,7 @@ class BaseAPI extends API {
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(headers);
|
||||
return headers;
|
||||
}
|
||||
|
||||
protected static override getHeaders(): Headers {
|
||||
|
||||
@@ -3,11 +3,7 @@ import { Page, expect, test } from "@playwright/test";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
test.describe("check live and health check of telemetry", () => {
|
||||
test("check if telemetry status is ok", async ({
|
||||
page,
|
||||
}: {
|
||||
page: Page;
|
||||
}) => {
|
||||
test("check if telemetry status is ok", async ({ page }: { page: Page }) => {
|
||||
page.setDefaultNavigationTimeout(120000); // 2 minutes
|
||||
await page.goto(
|
||||
`${URL.fromString(BASE_URL.toString())
|
||||
@@ -18,11 +14,7 @@ test.describe("check live and health check of telemetry", () => {
|
||||
expect(content).toContain('{"status":"ok"}');
|
||||
});
|
||||
|
||||
test("check if telemetry is ready", async ({
|
||||
page,
|
||||
}: {
|
||||
page: Page;
|
||||
}) => {
|
||||
test("check if telemetry is ready", async ({ page }: { page: Page }) => {
|
||||
page.setDefaultNavigationTimeout(120000); // 2 minutes
|
||||
await page.goto(
|
||||
`${URL.fromString(BASE_URL.toString())
|
||||
@@ -33,11 +25,7 @@ test.describe("check live and health check of telemetry", () => {
|
||||
expect(content).toContain('{"status":"ok"}');
|
||||
});
|
||||
|
||||
test("check if telemetry is live", async ({
|
||||
page,
|
||||
}: {
|
||||
page: Page;
|
||||
}) => {
|
||||
test("check if telemetry is live", async ({ page }: { page: Page }) => {
|
||||
page.setDefaultNavigationTimeout(120000); // 2 minutes
|
||||
await page.goto(
|
||||
`${URL.fromString(BASE_URL.toString())
|
||||
|
||||
@@ -28,17 +28,25 @@ const parseBatchSize: ParseBatchSizeFunction = (
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const TELEMETRY_LOG_FLUSH_BATCH_SIZE: number =
|
||||
parseBatchSize("TELEMETRY_LOG_FLUSH_BATCH_SIZE", 1000);
|
||||
export const TELEMETRY_LOG_FLUSH_BATCH_SIZE: number = parseBatchSize(
|
||||
"TELEMETRY_LOG_FLUSH_BATCH_SIZE",
|
||||
1000,
|
||||
);
|
||||
|
||||
export const TELEMETRY_METRIC_FLUSH_BATCH_SIZE: number =
|
||||
parseBatchSize("TELEMETRY_METRIC_FLUSH_BATCH_SIZE", 750);
|
||||
export const TELEMETRY_METRIC_FLUSH_BATCH_SIZE: number = parseBatchSize(
|
||||
"TELEMETRY_METRIC_FLUSH_BATCH_SIZE",
|
||||
750,
|
||||
);
|
||||
|
||||
export const TELEMETRY_TRACE_FLUSH_BATCH_SIZE: number =
|
||||
parseBatchSize("TELEMETRY_TRACE_FLUSH_BATCH_SIZE", 750);
|
||||
export const TELEMETRY_TRACE_FLUSH_BATCH_SIZE: number = parseBatchSize(
|
||||
"TELEMETRY_TRACE_FLUSH_BATCH_SIZE",
|
||||
750,
|
||||
);
|
||||
|
||||
export const TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE: number =
|
||||
parseBatchSize("TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE", 500);
|
||||
export const TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE: number = parseBatchSize(
|
||||
"TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE",
|
||||
500,
|
||||
);
|
||||
|
||||
/*
|
||||
* Some telemetry batches can be large and take >30s (BullMQ default lock) to process.
|
||||
|
||||
@@ -306,9 +306,7 @@ export default class OtelLogsIngestService extends OtelIngestBaseService {
|
||||
dbLogs.push(logRow);
|
||||
totalLogsProcessed++;
|
||||
|
||||
if (
|
||||
dbLogs.length >= TELEMETRY_LOG_FLUSH_BATCH_SIZE
|
||||
) {
|
||||
if (dbLogs.length >= TELEMETRY_LOG_FLUSH_BATCH_SIZE) {
|
||||
await this.flushLogsBuffer(dbLogs);
|
||||
}
|
||||
} catch (logError) {
|
||||
|
||||
@@ -312,8 +312,7 @@ export default class OtelMetricsIngestService extends OtelIngestBaseService {
|
||||
totalMetricsProcessed++;
|
||||
|
||||
if (
|
||||
dbMetrics.length >=
|
||||
TELEMETRY_METRIC_FLUSH_BATCH_SIZE
|
||||
dbMetrics.length >= TELEMETRY_METRIC_FLUSH_BATCH_SIZE
|
||||
) {
|
||||
await this.flushMetricsBuffer(dbMetrics);
|
||||
}
|
||||
|
||||
@@ -361,16 +361,12 @@ export default class OtelTracesIngestService extends OtelIngestBaseService {
|
||||
dbSpans.push(spanRow);
|
||||
totalSpansProcessed++;
|
||||
|
||||
if (
|
||||
dbSpans.length >=
|
||||
TELEMETRY_TRACE_FLUSH_BATCH_SIZE
|
||||
) {
|
||||
if (dbSpans.length >= TELEMETRY_TRACE_FLUSH_BATCH_SIZE) {
|
||||
await this.flushSpansBuffer(dbSpans);
|
||||
}
|
||||
|
||||
if (
|
||||
dbExceptions.length >=
|
||||
TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE
|
||||
dbExceptions.length >= TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE
|
||||
) {
|
||||
await this.flushExceptionsBuffer(dbExceptions);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user