mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
- Introduced DashboardDomain model with comprehensive fields for managing custom domains for dashboards. - Implemented DashboardDomainAPI for handling CNAME verification and SSL provisioning. - Created DashboardDomainService to manage domain-related operations, including SSL ordering and CNAME validation. - Added master password handling in DashboardAPI for enhanced security. - Defined constants for master password messages and cookie management.
428 lines
11 KiB
TypeScript
428 lines
11 KiB
TypeScript
import { ExpressRequest, ExpressResponse } from "./Express";
|
|
import Dictionary from "../../Types/Dictionary";
|
|
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";
|
|
import {
|
|
MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
|
MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
|
|
} from "../../Types/StatusPage/MasterPassword";
|
|
import {
|
|
DASHBOARD_MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
|
DASHBOARD_MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
|
|
} from "../../Types/Dashboard/MasterPassword";
|
|
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,
|
|
): Dictionary<string> {
|
|
const cookies: Dictionary<string> = {};
|
|
cookieString.split(";").forEach((cookie: string) => {
|
|
const parts: string[] = cookie.split("=");
|
|
const key: string = (parts[0] as string).trim() as string;
|
|
const value: string = parts[1] as string;
|
|
cookies[key] = value;
|
|
});
|
|
|
|
return cookies;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static setSSOCookie(data: {
|
|
user: User;
|
|
projectId: ObjectID;
|
|
expressResponse: ExpressResponse;
|
|
}): void {
|
|
const { user, projectId, expressResponse: res } = data;
|
|
|
|
const ssoToken: string = JSONWebToken.sign({
|
|
data: {
|
|
userId: user.id!,
|
|
projectId: projectId,
|
|
name: user.name!,
|
|
email: user.email,
|
|
isMasterAdmin: false,
|
|
isGeneralLogin: false,
|
|
},
|
|
expiresInSeconds: OneUptimeDate.getSecondsInDays(new PositiveNumber(30)),
|
|
});
|
|
|
|
CookieUtil.setCookie(res, CookieUtil.getUserSSOKey(projectId), ssoToken, {
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
|
httpOnly: true,
|
|
});
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static setUserCookie(data: {
|
|
expressResponse: ExpressResponse;
|
|
user: User;
|
|
isGlobalLogin: boolean;
|
|
sessionId: ObjectID;
|
|
refreshToken: string;
|
|
refreshTokenExpiresAt: Date;
|
|
accessTokenExpiresInSeconds?: number;
|
|
}): void {
|
|
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: {
|
|
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.
|
|
sessionId: sessionId,
|
|
},
|
|
expiresInSeconds: accessTokenExpiresInSeconds,
|
|
});
|
|
|
|
// Set a cookie with token.
|
|
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), token, {
|
|
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,
|
|
});
|
|
|
|
if (user.id) {
|
|
// set user id cookie
|
|
CookieUtil.setCookie(res, CookieName.UserID, user.id!.toString(), {
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
|
httpOnly: false,
|
|
});
|
|
}
|
|
|
|
if (user.email) {
|
|
// set user email cookie
|
|
CookieUtil.setCookie(
|
|
res,
|
|
CookieName.Email,
|
|
user.email?.toString() || "",
|
|
{
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
|
httpOnly: false,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (user.name) {
|
|
// set user name cookie
|
|
CookieUtil.setCookie(res, CookieName.Name, user.name?.toString() || "", {
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
|
httpOnly: false,
|
|
});
|
|
}
|
|
|
|
if (user.timezone) {
|
|
// set user timezone cookie
|
|
CookieUtil.setCookie(
|
|
res,
|
|
CookieName.Timezone,
|
|
user.timezone?.toString() || "",
|
|
{
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
|
httpOnly: false,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (user.isMasterAdmin) {
|
|
// set user isMasterAdmin cookie
|
|
CookieUtil.setCookie(
|
|
res,
|
|
CookieName.IsMasterAdmin,
|
|
user.isMasterAdmin?.toString() || "",
|
|
{
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
|
httpOnly: false,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (user.profilePictureId) {
|
|
// set user profile picture id cookie
|
|
CookieUtil.setCookie(
|
|
res,
|
|
CookieName.ProfilePicID,
|
|
user.profilePictureId?.toString() || "",
|
|
{
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
|
httpOnly: false,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
@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 setStatusPageMasterPasswordCookie(data: {
|
|
expressResponse: ExpressResponse;
|
|
statusPageId: ObjectID;
|
|
}): void {
|
|
const expiresInDays: PositiveNumber = new PositiveNumber(
|
|
MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
|
|
);
|
|
|
|
const token: string = JSONWebToken.signJsonPayload(
|
|
{
|
|
statusPageId: data.statusPageId.toString(),
|
|
type: MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
|
},
|
|
OneUptimeDate.getSecondsInDays(expiresInDays),
|
|
);
|
|
|
|
CookieUtil.setCookie(
|
|
data.expressResponse,
|
|
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
|
|
token,
|
|
{
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(expiresInDays),
|
|
httpOnly: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static setCookie(
|
|
res: ExpressResponse,
|
|
name: string | CookieName,
|
|
value: string,
|
|
options: CookieOptions,
|
|
): void {
|
|
const cookieOptions: CookieOptions = {
|
|
path: "/",
|
|
sameSite: "lax",
|
|
...options,
|
|
};
|
|
|
|
res.cookie(name, value, cookieOptions);
|
|
}
|
|
|
|
// get cookie with express request
|
|
|
|
@CaptureSpan()
|
|
public static getCookieFromExpressRequest(
|
|
req: ExpressRequest,
|
|
name: string,
|
|
): string | undefined {
|
|
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, {
|
|
path: "/",
|
|
sameSite: "lax",
|
|
});
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static removeStatusPageMasterPasswordCookie(
|
|
res: ExpressResponse,
|
|
statusPageId: ObjectID,
|
|
): void {
|
|
CookieUtil.removeCookie(
|
|
res,
|
|
CookieUtil.getStatusPageMasterPasswordKey(statusPageId),
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static setDashboardMasterPasswordCookie(data: {
|
|
expressResponse: ExpressResponse;
|
|
dashboardId: ObjectID;
|
|
}): void {
|
|
const expiresInDays: PositiveNumber = new PositiveNumber(
|
|
DASHBOARD_MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
|
|
);
|
|
|
|
const token: string = JSONWebToken.signJsonPayload(
|
|
{
|
|
dashboardId: data.dashboardId.toString(),
|
|
type: DASHBOARD_MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
|
},
|
|
OneUptimeDate.getSecondsInDays(expiresInDays),
|
|
);
|
|
|
|
CookieUtil.setCookie(
|
|
data.expressResponse,
|
|
CookieUtil.getDashboardMasterPasswordKey(data.dashboardId),
|
|
token,
|
|
{
|
|
maxAge: OneUptimeDate.getMillisecondsInDays(expiresInDays),
|
|
httpOnly: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static removeDashboardMasterPasswordCookie(
|
|
res: ExpressResponse,
|
|
dashboardId: ObjectID,
|
|
): void {
|
|
CookieUtil.removeCookie(
|
|
res,
|
|
CookieUtil.getDashboardMasterPasswordKey(dashboardId),
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static getDashboardMasterPasswordKey(id: ObjectID): string {
|
|
return `${CookieName.DashboardMasterPassword}-${id.toString()}`;
|
|
}
|
|
|
|
// get all cookies with express request
|
|
@CaptureSpan()
|
|
public static getAllCookies(req: ExpressRequest): Dictionary<string> {
|
|
return req.cookies || {};
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static getUserTokenKey(id?: ObjectID): string {
|
|
if (!id) {
|
|
return CookieName.Token;
|
|
}
|
|
|
|
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 getStatusPageMasterPasswordKey(id: ObjectID): string {
|
|
return `${CookieName.StatusPageMasterPassword}-${id.toString()}`;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static getUserSSOKey(id: ObjectID): string {
|
|
return `${this.getSSOKey()}${id.toString()}`;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public static getSSOKey(): string {
|
|
return `sso-`;
|
|
}
|
|
|
|
// delete all cookies.
|
|
@CaptureSpan()
|
|
public static removeAllCookies(
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
): void {
|
|
const cookies: Dictionary<string> = this.getAllCookies(req);
|
|
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());
|
|
}
|
|
}
|