This commit is contained in:
Simon Larsen
2022-12-07 12:14:17 +05:30
parent 5bce49bf2f
commit ef948319d3
51 changed files with 895 additions and 824 deletions

View File

@@ -15,7 +15,7 @@ import { DASHBOARD_URL } from 'CommonUI/src/Config';
const LoginPage: FunctionComponent = () => {
const apiUrl: URL = LOGIN_API_URL;
if (UserUtil.isLoggedIn()) {
Navigation.navigate(DASHBOARD_URL);
}

View File

@@ -15,7 +15,7 @@ import { SIGNUP_API_URL } from '../Utils/ApiPaths';
const RegisterPage: FunctionComponent = () => {
const apiUrl: URL = SIGNUP_API_URL;
if (UserUtil.isLoggedIn()) {
Navigation.navigate(DASHBOARD_URL);
}

View File

@@ -4,7 +4,7 @@ enum Protocol {
MONGO_DB = 'mongodb://',
WS = 'ws://',
WSS = 'wss://',
MAIL = 'mailto:'
MAIL = 'mailto:',
}
export default Protocol;

View File

@@ -24,7 +24,6 @@ export default class URL extends DatabaseProperty {
this._params = v;
}
private _email!: Email;
public get email(): Email {
return this._email;
@@ -33,7 +32,6 @@ export default class URL extends DatabaseProperty {
this._email = v;
}
private _hostname!: Hostname;
public get hostname(): Hostname {
return this._hostname;
@@ -58,8 +56,11 @@ export default class URL extends DatabaseProperty {
) {
super();
if (typeof hostname === Typeof.String && Email.isValid(hostname as string)) {
this.email = new Email(hostname as string)
if (
typeof hostname === Typeof.String &&
Email.isValid(hostname as string)
) {
this.email = new Email(hostname as string);
} else if (hostname instanceof Email) {
this.email = hostname;
} else if (hostname instanceof Hostname) {
@@ -93,7 +94,9 @@ export default class URL extends DatabaseProperty {
}
public override toString(): string {
let urlString: string = `${this.protocol}${this.hostname || this.email}`;
let urlString: string = `${this.protocol}${
this.hostname || this.email
}`;
if (!this.email) {
if (this.route && this.route.toString().startsWith('/')) {
urlString += this.route.toString();

View File

@@ -31,7 +31,7 @@ enum ColumnType {
BigNumber = 'bigint',
Markdown = 'text',
File = 'bytea',
JSON = 'jsonb'
JSON = 'jsonb',
}
export default ColumnType;

View File

@@ -32,7 +32,7 @@ enum ColumnType {
BigNumber,
Entity,
EntityArray,
JSON
JSON,
}
export default ColumnType;

View File

@@ -4,7 +4,7 @@ enum EmailTemplateType {
EmailVerified = 'EmailVerified.hbs',
PasswordChanged = 'PasswordChanged.hbs',
InviteMember = 'InviteMember.hbs',
EmailChanged = 'EmailChanged.hbs'
EmailChanged = 'EmailChanged.hbs',
}
export default EmailTemplateType;

View File

@@ -227,7 +227,6 @@ export class PermissionHelper {
permissions1: Array<Permission>,
permissions2: Array<Permission>
): boolean {
if (!permissions1) {
permissions1 = [];
}

View File

@@ -206,7 +206,7 @@ export default class API {
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers,
params?: Dictionary<string>,
params?: Dictionary<string>
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
const apiHeaders: Headers = this.getHeaders(headers);

View File

@@ -17,12 +17,15 @@ export const DatabaseHost: Hostname = Hostname.fromString(
process.env['DATABASE_HOST'] || 'postgres'
);
export const DatabasePort: Port = new Port(process.env['DATABASE_PORT'] || '5432');
export const DatabasePort: Port = new Port(
process.env['DATABASE_PORT'] || '5432'
);
export const DatabaseUsername: string =
process.env['DATABASE_USERNAME'] || 'oneuptimedbuser';
export const DatabasePassword: string = process.env['DATABASE_PASSWORD'] || 'password';
export const DatabasePassword: string =
process.env['DATABASE_PASSWORD'] || 'password';
export const DatabaseName: string =
process.env['DATABASE_NAME'] || 'oneuptimedb';
@@ -91,7 +94,8 @@ export const HttpProtocol: Protocol = (
// Redis does not require password.
export const RedisHostname: string = process.env['REDIS_HOST'] || 'redis';
export const RedisPassword: string = process.env['REDIS_PASSWORD'] || 'password';
export const RedisPassword: string =
process.env['REDIS_PASSWORD'] || 'password';
export const RedisPort: Port = new Port(process.env['REDIS_PORT'] || '6379');
export const DashboardApiRoute: Route = new Route(
@@ -116,7 +120,9 @@ export const IntegrationRoute: Route = new Route(
process.env['INTEGRATION_ROUTE'] || '/integration'
);
export const HelmRoute: Route = new Route(process.env['HELMCHART_ROUTE'] || '/helm-chart');
export const HelmRoute: Route = new Route(
process.env['HELMCHART_ROUTE'] || '/helm-chart'
);
export const AccountsRoute: Route = new Route(
process.env['ACCOUNTS_ROUTE'] || '/accounts'
);

View File

@@ -42,69 +42,73 @@ export class Service extends DatabaseService<IncidentStateTimeline> {
props: onCreate.createBy.props,
});
// TODO: DELETE THIS WHEN WORKFLOW IS IMPLEMENMTED.
// TODO: DELETE THIS WHEN WORKFLOW IS IMPLEMENMTED.
// check if this is resolved state, and if it is then resolve all the monitors.
const isResolvedState: IncidentState | null = await IncidentStateService.findOneBy({
query: {
_id: createdItem.incidentStateId.toString()!,
isResolvedState: true
},
props: {
isRoot: true
},
select: {
_id: true
}
});
const isResolvedState: IncidentState | null =
await IncidentStateService.findOneBy({
query: {
_id: createdItem.incidentStateId.toString()!,
isResolvedState: true,
},
props: {
isRoot: true,
},
select: {
_id: true,
},
});
if (isResolvedState) {
// resolve all the monitors.
const incident: Incident | null= await IncidentService.findOneBy({
// resolve all the monitors.
const incident: Incident | null = await IncidentService.findOneBy({
query: {
_id: createdItem.incidentId?.toString(),
},
select: {
_id: true,
projectId: true
projectId: true,
},
populate: {
monitors: {
_id: true
}
_id: true,
},
},
props: {
isRoot: true,
isRoot: true,
},
});
if (incident && incident.monitors && incident.monitors.length > 0) {
// get resolved monitor state.
const resolvedMonitorState: MonitorStatus | null = await MonitorStatusService.findOneBy({
query: {
projectId: incident.projectId!,
isOperationalState: true
},
props: {
isRoot: true,
},
select: {
_id: true
}
});
// get resolved monitor state.
const resolvedMonitorState: MonitorStatus | null =
await MonitorStatusService.findOneBy({
query: {
projectId: incident.projectId!,
isOperationalState: true,
},
props: {
isRoot: true,
},
select: {
_id: true,
},
});
if (resolvedMonitorState) {
for (const monitor of incident.monitors) {
const monitorStausTimeline: MonitorStatusTimeline = new MonitorStatusTimeline();
const monitorStausTimeline: MonitorStatusTimeline =
new MonitorStatusTimeline();
monitorStausTimeline.monitorId = monitor.id!;
monitorStausTimeline.projectId = incident.projectId!;
monitorStausTimeline.monitorStatusId = resolvedMonitorState.id!;
monitorStausTimeline.monitorStatusId =
resolvedMonitorState.id!;
await MonitorStatusTimelineService.create({
data: monitorStausTimeline,
props: {
isRoot: true
}
isRoot: true,
},
});
}
}

View File

@@ -459,7 +459,7 @@ export class Service extends DatabaseService<Model> {
data: ownerTeamMember,
props: {
isRoot: true,
ignoreHooks: true
ignoreHooks: true,
},
});
@@ -537,7 +537,6 @@ export class Service extends DatabaseService<Model> {
},
});
await TeamMemberService.refreshTokens(
createdItem.createdByUserId!,
createdItem.id!

View File

@@ -41,79 +41,89 @@ export class Service extends DatabaseService<ScheduledMaintenanceStateTimeline>
createdItem.scheduledMaintenanceStateId,
},
props: {
isRoot: true,
isRoot: true,
},
});
// TODO: DELETE THIS WHEN WORKFLOW IS IMPLEMENMTED.
// TODO: DELETE THIS WHEN WORKFLOW IS IMPLEMENMTED.
// check if this is resolved state, and if it is then resolve all the monitors.
const isResolvedState: ScheduledMaintenanceState | null = await ScheduledMaintenanceStateService.findOneBy({
query: {
_id: createdItem.scheduledMaintenanceStateId.toString()!,
isResolvedState: true
},
props: {
isRoot: true
},
select: {
_id: true
}
});
if (isResolvedState) {
// resolve all the monitors.
const scheduledMaintenanceService: ScheduledMaintenance | null= await ScheduledMaintenanceService.findOneBy({
const isResolvedState: ScheduledMaintenanceState | null =
await ScheduledMaintenanceStateService.findOneBy({
query: {
_id: createdItem.scheduledMaintenanceId?.toString(),
_id: createdItem.scheduledMaintenanceStateId.toString()!,
isResolvedState: true,
},
props: {
isRoot: true,
},
select: {
_id: true,
projectId: true
},
populate: {
monitors: {
_id: true
}
},
props: {
isRoot: true,
},
});
if (scheduledMaintenanceService && scheduledMaintenanceService.monitors && scheduledMaintenanceService.monitors.length > 0) {
// get resolved monitor state.
const resolvedMonitorState: MonitorStatus | null = await MonitorStatusService.findOneBy({
if (isResolvedState) {
// resolve all the monitors.
const scheduledMaintenanceService: ScheduledMaintenance | null =
await ScheduledMaintenanceService.findOneBy({
query: {
projectId: scheduledMaintenanceService.projectId!,
isOperationalState: true
_id: createdItem.scheduledMaintenanceId?.toString(),
},
select: {
_id: true,
projectId: true,
},
populate: {
monitors: {
_id: true,
},
},
props: {
isRoot: true,
},
select: {
_id: true
}
});
if (
scheduledMaintenanceService &&
scheduledMaintenanceService.monitors &&
scheduledMaintenanceService.monitors.length > 0
) {
// get resolved monitor state.
const resolvedMonitorState: MonitorStatus | null =
await MonitorStatusService.findOneBy({
query: {
projectId: scheduledMaintenanceService.projectId!,
isOperationalState: true,
},
props: {
isRoot: true,
},
select: {
_id: true,
},
});
if (resolvedMonitorState) {
for (const monitor of scheduledMaintenanceService.monitors) {
const monitorStausTimeline: MonitorStatusTimeline = new MonitorStatusTimeline();
const monitorStausTimeline: MonitorStatusTimeline =
new MonitorStatusTimeline();
monitorStausTimeline.monitorId = monitor.id!;
monitorStausTimeline.projectId = scheduledMaintenanceService.projectId!;
monitorStausTimeline.monitorStatusId = resolvedMonitorState.id!;
monitorStausTimeline.projectId =
scheduledMaintenanceService.projectId!;
monitorStausTimeline.monitorStatusId =
resolvedMonitorState.id!;
await MonitorStatusTimelineService.create({
data: monitorStausTimeline,
props: {
isRoot: true
}
isRoot: true,
},
});
}
}
}
}
return createdItem;
}

View File

@@ -7,12 +7,11 @@ import API from 'Common/Utils/API';
import { HttpProtocol, WorkerHostname } from '../Config';
export default class StatusPageCertificateService {
public static async add(
domain: string
): Promise<HTTPResponse<EmptyResponseData>> {
const body: JSONObject = {
domain: domain
domain: domain,
};
return await API.post<EmptyResponseData>(
@@ -25,7 +24,7 @@ export default class StatusPageCertificateService {
domain: string
): Promise<HTTPResponse<EmptyResponseData>> {
const body: JSONObject = {
domain: domain
domain: domain,
};
return await API.delete<EmptyResponseData>(
@@ -34,11 +33,9 @@ export default class StatusPageCertificateService {
);
}
public static async get(
domain: string
): Promise<HTTPResponse<JSONObject>> {
public static async get(domain: string): Promise<HTTPResponse<JSONObject>> {
const body: JSONObject = {
domain: domain
domain: domain,
};
return await API.get<JSONObject>(

View File

@@ -33,7 +33,9 @@ export class Service extends DatabaseService<Model> {
});
if (!domain?.isVerified) {
throw new BadDataException("This domain is not verified. Please verify it by going to Settings > Domains");
throw new BadDataException(
'This domain is not verified. Please verify it by going to Settings > Domains'
);
}
if (domain) {
@@ -46,14 +48,16 @@ export class Service extends DatabaseService<Model> {
return { createBy, carryForward: null };
}
protected override async onBeforeDelete(deleteBy: DeleteBy<Model>): Promise<OnDelete<Model>> {
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>
): Promise<OnDelete<Model>> {
const domains: Array<Model> = await this.findBy({
query: {
...deleteBy.query,
isAddedtoGreenlock: true
isAddedtoGreenlock: true,
},
populate: {},
skip: 0,
skip: 0,
limit: LIMIT_MAX,
select: { fullDomain: true },
props: {
@@ -61,19 +65,20 @@ export class Service extends DatabaseService<Model> {
},
});
return { deleteBy, carryForward: domains };
}
protected override async onDeleteSuccess(onDelete: OnDelete<Model>, _itemIdsBeforeDelete: ObjectID[]): Promise<OnDelete<Model>> {
protected override async onDeleteSuccess(
onDelete: OnDelete<Model>,
_itemIdsBeforeDelete: ObjectID[]
): Promise<OnDelete<Model>> {
for (const domain of onDelete.carryForward) {
await StatusPageCertificateService.remove(domain.fullDomain as string);
await StatusPageCertificateService.remove(
domain.fullDomain as string
);
}
return onDelete;
}
}
export default new Service();

View File

@@ -40,7 +40,6 @@ export class Service extends DatabaseService<TeamMember> {
protected override async onBeforeCreate(
createBy: CreateBy<TeamMember>
): Promise<OnCreate<TeamMember>> {
createBy.data.hasAcceptedInvitation = false;
if (createBy.miscDataProps && createBy.miscDataProps['email']) {
@@ -90,12 +89,12 @@ export class Service extends DatabaseService<TeamMember> {
}
}
//check if this user is already ivnited.
//check if this user is already ivnited.
const member: TeamMember | null = await this.findOneBy({
query: {
userId: createBy.data.userId!,
teamId: createBy.data.teamId!
teamId: createBy.data.teamId!,
},
props: {
isRoot: true,
@@ -106,7 +105,9 @@ export class Service extends DatabaseService<TeamMember> {
});
if (member) {
throw new BadDataException("This user has already been invited to this team");
throw new BadDataException(
'This user has already been invited to this team'
);
}
return { createBy, carryForward: null };
@@ -179,7 +180,7 @@ export class Service extends DatabaseService<TeamMember> {
projectId: true,
team: true,
teamId: true,
hasAcceptedInvitation: true
hasAcceptedInvitation: true,
},
limit: LIMIT_MAX,
skip: 0,
@@ -197,15 +198,14 @@ export class Service extends DatabaseService<TeamMember> {
// check if there's one member in the team.
for (const member of members) {
if (member.team?.shouldHaveAtleastOneMember) {
if (!member.hasAcceptedInvitation) {
continue;
continue;
}
const membersInTeam: PositiveNumber = await this.countBy({
query: {
teamId: member.teamId!,
hasAcceptedInvitation: true
hasAcceptedInvitation: true,
},
skip: 0,
limit: LIMIT_MAX,

View File

@@ -36,12 +36,11 @@ export class Service extends DatabaseService<Model> {
});
}
protected override async onBeforeUpdate(updateBy: UpdateBy<Model>): Promise<OnUpdate<Model>> {
protected override async onBeforeUpdate(
updateBy: UpdateBy<Model>
): Promise<OnUpdate<Model>> {
if (updateBy.data.password || updateBy.data.email) {
const users = await this.findBy({
const users: Array<Model> = await this.findBy({
query: updateBy.query,
select: {
_id: true,
@@ -51,16 +50,18 @@ export class Service extends DatabaseService<Model> {
isRoot: true,
},
limit: LIMIT_MAX,
skip: 0
})
skip: 0,
});
return { updateBy, carryForward: users };
}
return { updateBy, carryForward: [] };
}
protected override async onUpdateSuccess(onUpdate: OnUpdate<Model>, _updatedItemIds: ObjectID[]): Promise<OnUpdate<Model>> {
protected override async onUpdateSuccess(
onUpdate: OnUpdate<Model>,
_updatedItemIds: ObjectID[]
): Promise<OnUpdate<Model>> {
if (onUpdate && onUpdate.updateBy.data.password) {
for (const user of onUpdate.carryForward) {
// password changed, send password changed mail
@@ -78,26 +79,29 @@ export class Service extends DatabaseService<Model> {
}
if (onUpdate && onUpdate.updateBy.data.email) {
const newUsers = await this.findBy({
const newUsers: Array<Model> = await this.findBy({
query: onUpdate.updateBy.query,
select: {
_id: true,
email: true,
name: true
name: true,
},
props: {
isRoot: true,
},
limit: LIMIT_MAX,
skip: 0
})
skip: 0,
});
for (const user of onUpdate.carryForward) {
const newUser: Model | undefined = newUsers.find((u: Model) => {
return u._id?.toString() === user._id.toString();
});
const newUser = newUsers.find((u) => u._id?.toString() === user._id.toString());
if (newUser && newUser.email?.toString() !== user.email.toString()) {
if (
newUser &&
newUser.email?.toString() !== user.email.toString()
) {
// password changed, send password changed mail
const generatedToken: ObjectID = ObjectID.generate();
@@ -106,7 +110,8 @@ export class Service extends DatabaseService<Model> {
emailVerificationToken.userId = user?.id!;
emailVerificationToken.email = newUser?.email!;
emailVerificationToken.token = generatedToken;
emailVerificationToken.expires = OneUptimeDate.getOneDayAfter();
emailVerificationToken.expires =
OneUptimeDate.getOneDayAfter();
await EmailVerificationTokenService.create({
data: emailVerificationToken,
@@ -117,7 +122,8 @@ export class Service extends DatabaseService<Model> {
MailService.sendMail({
toEmail: newUser.email!,
subject: 'You have changed your email. Please verify your email.',
subject:
'You have changed your email. Please verify your email.',
templateType: EmailTemplateType.EmailChanged,
vars: {
name: newUser.name!.toString(),
@@ -136,20 +142,17 @@ export class Service extends DatabaseService<Model> {
await this.updateBy({
query: {
_id: user.id.toString()
_id: user.id.toString(),
},
data: {
isEmailVerified: false
isEmailVerified: false,
},
props: {
isRoot: true,
ignoreHooks: true
}
ignoreHooks: true,
},
});
}
}
}

View File

@@ -9,7 +9,6 @@ export interface CardButtonSchema {
disabled?: boolean | undefined;
icon: IconProp;
isLoading?: undefined | boolean;
}
export interface ComponentProps {
@@ -20,7 +19,7 @@ export interface ComponentProps {
children?: undefined | Array<ReactElement> | ReactElement;
cardBodyStyle?: undefined | CSSProperties;
className?: string | undefined;
style?: React.CSSProperties | undefined
style?: React.CSSProperties | undefined;
}
const Card: FunctionComponent<ComponentProps> = (

View File

@@ -7,7 +7,7 @@ import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
export interface ComponentProps<TBaseModel extends BaseModel> {
alertType: AlertType;
modelType: { new(): TBaseModel };
modelType: { new (): TBaseModel };
singularName: string;
pluralName: string;
query: Query<TBaseModel>;
@@ -49,7 +49,7 @@ const CounterModelAlert: Function = <TBaseModel extends BaseModel>(
try {
setError(
(err as HTTPErrorResponse).message ||
'Server Error. Please try again'
'Server Error. Please try again'
);
} catch (e) {
setError('Server Error. Please try again');
@@ -82,8 +82,9 @@ const CounterModelAlert: Function = <TBaseModel extends BaseModel>(
style={props.style}
onClick={props.onClick}
type={props.alertType}
title={`${count} ${count > 1 ? props.pluralName : props.singularName
}`}
title={`${count} ${
count > 1 ? props.pluralName : props.singularName
}`}
/>
);
};

View File

@@ -1103,7 +1103,10 @@ const BasicForm: Function = <T extends Object>(
type={ButtonTypes.Submit}
id={`${props.id}-submit-button`}
isLoading={props.isLoading || false}
buttonStyle={props.submitButtonStyleType || ButtonStyleType.PRIMARY}
buttonStyle={
props.submitButtonStyleType ||
ButtonStyleType.PRIMARY
}
style={{
width: props.maxPrimaryButtonWidth
? '100%'

View File

@@ -44,13 +44,13 @@ export enum FormType {
}
export interface ComponentProps<TBaseModel extends BaseModel> {
modelType: { new(): TBaseModel };
modelType: { new (): TBaseModel };
id: string;
onValidate?:
| undefined
| ((
values: FormValues<TBaseModel>
) => FormikErrors<FormValues<TBaseModel>>);
| undefined
| ((
values: FormValues<TBaseModel>
) => FormikErrors<FormValues<TBaseModel>>);
fields: Fields<TBaseModel>;
submitButtonText?: undefined | string;
title?: undefined | string;
@@ -59,8 +59,8 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
footer: ReactElement;
onCancel?: undefined | (() => void);
onSuccess?:
| undefined
| ((data: TBaseModel | JSONObjectOrArray | Array<TBaseModel>) => void);
| undefined
| ((data: TBaseModel | JSONObjectOrArray | Array<TBaseModel>) => void);
cancelButtonText?: undefined | string;
maxPrimaryButtonWidth?: undefined | boolean;
apiUrl?: undefined | URL;
@@ -74,7 +74,7 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
onError?: ((error: string) => void) | undefined;
onBeforeCreate?: ((item: TBaseModel) => Promise<TBaseModel>) | undefined;
saveRequestOptions?: RequestOptions | undefined;
doNotFetchExistingModel?: boolean | undefined
doNotFetchExistingModel?: boolean | undefined;
}
const ModelForm: Function = <TBaseModel extends BaseModel>(
@@ -229,7 +229,7 @@ const ModelForm: Function = <TBaseModel extends BaseModel>(
isModelArray = true;
idArray.push(
(itemInArray as any as JSONObject)[
'_id'
'_id'
] as string
);
}
@@ -308,7 +308,7 @@ const ModelForm: Function = <TBaseModel extends BaseModel>(
try {
setError(
(err as HTTPErrorResponse).message ||
'Server Error. Please try again'
'Server Error. Please try again'
);
} catch (e) {
setError('Server Error. Please try again');
@@ -319,7 +319,11 @@ const ModelForm: Function = <TBaseModel extends BaseModel>(
};
useAsyncEffect(async () => {
if (props.modelIdToEdit && props.formType === FormType.Update && !props.doNotFetchExistingModel) {
if (
props.modelIdToEdit &&
props.formType === FormType.Update &&
!props.doNotFetchExistingModel
) {
// get item.
setLoading(true);
setIsFetching(true);
@@ -412,7 +416,7 @@ const ModelForm: Function = <TBaseModel extends BaseModel>(
Array.isArray(valuesToSend[key]) &&
(valuesToSend[key] as Array<any>).length > 0 &&
typeof (valuesToSend[key] as Array<any>)[0] ===
Typeof.String
Typeof.String
) {
const arr: Array<BaseModel> = [];
for (const id of valuesToSend[key] as Array<string>) {
@@ -449,7 +453,7 @@ const ModelForm: Function = <TBaseModel extends BaseModel>(
} catch (err) {
setError(
(err as HTTPErrorResponse).message ||
'Server Error. Please try again'
'Server Error. Please try again'
);
}

View File

@@ -8,7 +8,7 @@ export interface ComponentProps {
title: string;
badge?: undefined | number;
route?: Route | undefined;
onClick?: (()=> void) | undefined;
onClick?: (() => void) | undefined;
icon: IconProp;
iconColor?: undefined | Color;
}

View File

@@ -5,7 +5,7 @@ import React, { FunctionComponent, ReactElement } from 'react';
import URLFromProject from 'Common/Types/API/URL';
export interface ComponentProps {
onClick?: (() => void | undefined);
onClick?: () => void | undefined;
imageUrl?: URLFromProject | Route | undefined;
height?: number | undefined;
file?: File | undefined;

View File

@@ -68,7 +68,11 @@ const CardModelDetail: Function = <TBaseModel extends BaseModel>(
return (
<>
<Card {...props.cardProps} className={props.className} buttons={cardButtons}>
<Card
{...props.cardProps}
className={props.className}
buttons={cardButtons}
>
<ModelDetail
refresher={refresher}
{...props.modelDetailProps}

View File

@@ -40,7 +40,6 @@ const ModelDetail: Function = <TBaseModel extends BaseModel>(
JSONObject | undefined
>(undefined);
const getSelectFields: Function = (): Select<TBaseModel> => {
const select: Select<TBaseModel> = {};
for (const field of props.fields) {
@@ -84,11 +83,11 @@ const ModelDetail: Function = <TBaseModel extends BaseModel>(
const userPermissions: Array<Permission> =
PermissionUtil.getAllPermissions();
const model = new props.modelType();
const model: BaseModel = new props.modelType();
const accessControl: Dictionary<ColumnAccessControl> =
model.getColumnAccessControlForAllColumns() || {};
model.getColumnAccessControlForAllColumns() || {};
const fieldsToSet: Array<Field<TBaseModel>> = [];
@@ -144,7 +143,6 @@ const ModelDetail: Function = <TBaseModel extends BaseModel>(
setFields(fieldsToSet);
};
useEffect(() => {
if (props.modelType) {
setDetailFields();
@@ -152,7 +150,6 @@ const ModelDetail: Function = <TBaseModel extends BaseModel>(
}, [onBeforeFetchData, props.modelType]);
const fetchItem: Function = async (): Promise<void> => {
// get item.
setIsLoading(true);
props.onLoadingChange && props.onLoadingChange(true);

View File

@@ -61,14 +61,14 @@ export enum ShowTableAs {
}
export interface ComponentProps<TBaseModel extends BaseModel> {
modelType: { new(): TBaseModel };
modelType: { new (): TBaseModel };
id: string;
onFetchInit?:
| undefined
| ((pageNumber: number, itemsOnPage: number) => void);
| undefined
| ((pageNumber: number, itemsOnPage: number) => void);
onFetchSuccess?:
| undefined
| ((data: Array<TBaseModel>, totalCount: number) => void);
| undefined
| ((data: Array<TBaseModel>, totalCount: number) => void);
cardProps?: CardComponentProps | undefined;
columns: Columns<TBaseModel>;
selectMoreFields?: Select<TBaseModel>;
@@ -195,8 +195,8 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
fieldType: column.type,
getElement: column.getElement
? (item: JSONObject): ReactElement => {
return column.getElement!(item, onBeforeFetchData);
}
return column.getElement!(item, onBeforeFetchData);
}
: undefined,
});
@@ -228,7 +228,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
try {
setErrorModalText(
(err as HTTPErrorResponse).message ||
'Server Error. Please try again'
'Server Error. Please try again'
);
} catch (e) {
setErrorModalText('Server Error. Please try again');
@@ -325,8 +325,10 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
if (column.tooltipText) {
classicColumn.tooltipText = (item: JSONObject): string => {
return column.tooltipText!(BaseModel.fromJSONObject(item, props.modelType));
}
return column.tooltipText!(
BaseModel.fromJSONObject(item, props.modelType)
);
};
}
}
@@ -335,7 +337,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
try {
setTableFilterError(
(err as HTTPErrorResponse).message ||
'Server Error. Please try again'
'Server Error. Please try again'
);
} catch (e) {
setTableFilterError('Server Error. Please try again');
@@ -371,8 +373,8 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
getSelect(),
sortBy
? {
[sortBy as any]: sortOrder,
}
[sortBy as any]: sortOrder,
}
: {},
getPopulate(),
props.fetchRequestOptions
@@ -384,7 +386,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
try {
setError(
(err as HTTPErrorResponse).message ||
'Server Error. Please try again'
'Server Error. Please try again'
);
} catch (e) {
setError('Server Error. Please try again');
@@ -497,8 +499,9 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
showTableAs !== ShowTableAs.OrderedStatesList
) {
headerbuttons.push({
title: `${props.createVerb || 'Create'} ${props.singularName || model.singularName
}`,
title: `${props.createVerb || 'Create'} ${
props.singularName || model.singularName
}`,
buttonStyle: ButtonStyleType.OUTLINE,
onClick: () => {
setModalType(ModalType.Create);
@@ -647,13 +650,15 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
const key: string | null = getColumnKey(column);
if (hasPermission) {
let tooltipText: ((item: JSONObject) => string) | undefined = undefined;
let tooltipText: ((item: JSONObject) => string) | undefined =
undefined;
if (column.tooltipText) {
tooltipText = (item: JSONObject): string => {
return column.tooltipText!(BaseModel.fromJSONObject(item, props.modelType));
}
return column.tooltipText!(
BaseModel.fromJSONObject(item, props.modelType)
);
};
}
columns.push({
@@ -662,7 +667,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
key: column.selectedProperty
? key + '.' + column.selectedProperty
: key,
tooltipText
tooltipText,
});
if (key) {
@@ -970,9 +975,9 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
let getTitleElement:
| ((
item: JSONObject,
onBeforeFetchData?: JSONObject | undefined
) => ReactElement)
item: JSONObject,
onBeforeFetchData?: JSONObject | undefined
) => ReactElement)
| undefined = undefined;
let getDescriptionElement:
| ((item: JSONObject) => ReactElement)
@@ -1016,10 +1021,10 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
onCreateNewItem={
props.isCreateable
? (order: number) => {
setOrderedStatesListNewItemOrder(order);
setModalType(ModalType.Create);
setShowModal(true);
}
setOrderedStatesListNewItemOrder(order);
setModalType(ModalType.Create);
setShowModal(true);
}
: undefined
}
singularLabel={
@@ -1083,8 +1088,9 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
}}
>
<Pill
text={`${new props.modelType().readBillingPlan
} Plan`}
text={`${
new props.modelType().readBillingPlan
} Plan`}
color={Yellow}
/>
</span>
@@ -1156,8 +1162,9 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
<ModelFormModal<TBaseModel>
title={
modalType === ModalType.Create
? `${props.createVerb || 'Create'} New ${props.singularName || model.singularName
}`
? `${props.createVerb || 'Create'} New ${
props.singularName || model.singularName
}`
: `Edit ${props.singularName || model.singularName}`
}
initialValues={
@@ -1170,8 +1177,9 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
}}
submitButtonText={
modalType === ModalType.Create
? `${props.createVerb || 'Create'} ${props.singularName || model.singularName
}`
? `${props.createVerb || 'Create'} ${
props.singularName || model.singularName
}`
: `Save Changes`
}
onSuccess={(_item: TBaseModel) => {

View File

@@ -68,7 +68,9 @@ const TableRow: FunctionComponent<ComponentProps> = (
}}
onClick={() => {
if (column.tooltipText) {
setTooltipModalText(column.tooltipText(props.item));
setTooltipModalText(
column.tooltipText(props.item)
);
}
}}
>
@@ -77,25 +79,27 @@ const TableRow: FunctionComponent<ComponentProps> = (
props.item[column.key] ? (
OneUptimeDate.getDateAsLocalFormattedString(
props.item[
column.key
column.key
] as string,
true
)
) : (
''
)
) : column.type === FieldType.DateTime ? (
) : column.type ===
FieldType.DateTime ? (
props.item[column.key] ? (
OneUptimeDate.getDateAsLocalFormattedString(
props.item[
column.key
column.key
] as string,
false
)
) : (
''
)
) : column.type === FieldType.Boolean ? (
) : column.type ===
FieldType.Boolean ? (
props.item[column.key] ? (
<Icon
icon={IconProp.True}
@@ -130,7 +134,9 @@ const TableRow: FunctionComponent<ComponentProps> = (
<ConfirmModal
title={`Error`}
description={error}
submitButtonText={'Close'}
submitButtonText={
'Close'
}
onSubmit={() => {
return setError('');
}}
@@ -156,9 +162,9 @@ const TableRow: FunctionComponent<ComponentProps> = (
style={
i > 0
? {
marginLeft:
'10px',
}
marginLeft:
'10px',
}
: {}
}
key={i}
@@ -167,14 +173,18 @@ const TableRow: FunctionComponent<ComponentProps> = (
buttonSize={
ButtonSize.Small
}
title={button.title}
icon={button.icon}
title={
button.title
}
icon={
button.icon
}
buttonStyle={
button.buttonStyleType
}
isLoading={
isButtonLoading[
i
i
]
}
onClick={() => {
@@ -230,8 +240,6 @@ const TableRow: FunctionComponent<ComponentProps> = (
</td>
);
})}
</tr>
{tooltipModalText && (
<ConfirmModal

View File

@@ -141,4 +141,5 @@ export const HOME_URL: URL = new URL(HTTP_PROTOCOL, HOME_HOSTNAME, HOME_ROUTE);
export const SubscriptionPlans: Array<SubscriptionPlan> =
SubscriptionPlan.getSubscriptionPlans();
export const StatusPageCNameRecord: string = env('STATUS_PAGE_CNAME_RECORD') || '';
export const StatusPageCNameRecord: string =
env('STATUS_PAGE_CNAME_RECORD') || '';

View File

@@ -58,8 +58,7 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
null
);
const [showProfileModal, setShowProfileModal] =
useState<boolean>(false);
const [showProfileModal, setShowProfileModal] = useState<boolean>(false);
useAsyncEffect(async () => {
if (
@@ -83,7 +82,7 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
leftComponents={
<>
{props.projects.length === 0 && (
<Logo onClick={() => { }} />
<Logo onClick={() => {}} />
)}
<ProjectPicker
showProjectModal={props.showProjectModal}
@@ -97,7 +96,7 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
onChange={(_value: string) => {}}
/> */}
<div
className='flex'
className="flex"
style={{
marginLeft: '15px',
marginTop: '15px',
@@ -120,7 +119,7 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
setShowProjectInvitationModal(true);
}}
style={{
marginRight: "10px"
marginRight: '10px',
}}
/>
<CounterModelAlert<Incident>
@@ -140,7 +139,7 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
setShowActiveIncidentsModal(true);
}}
style={{
marginRight: "10px"
marginRight: '10px',
}}
/>
@@ -155,16 +154,17 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
title={`Trial ends in ${OneUptimeDate.getNumberOfDaysBetweenDatesInclusive(
OneUptimeDate.getCurrentDate(),
props.selectedProject?.trialEndsAt!
)} ${OneUptimeDate.getNumberOfDaysBetweenDatesInclusive(
OneUptimeDate.getCurrentDate(),
props.selectedProject
?.trialEndsAt!
) > 1
? 'days'
: 'day'
}`}
)} ${
OneUptimeDate.getNumberOfDaysBetweenDatesInclusive(
OneUptimeDate.getCurrentDate(),
props.selectedProject
?.trialEndsAt!
) > 1
? 'days'
: 'day'
}`}
style={{
marginRight: "10px"
marginRight: '10px',
}}
/>
)}
@@ -175,23 +175,23 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
<>
{/* <Notifications /> */}
{BILLING_ENABLED &&
props.selectedProject?.id &&
props.selectedProject.paymentProviderPlanId &&
!SubscriptionPlan.isFreePlan(
props.selectedProject.paymentProviderPlanId
) &&
!SubscriptionPlan.isCustomPricingPlan(
props.selectedProject.paymentProviderPlanId
) &&
!isPaymentMethodCountLoading &&
paymentMethodCount === 0 ? (
props.selectedProject?.id &&
props.selectedProject.paymentProviderPlanId &&
!SubscriptionPlan.isFreePlan(
props.selectedProject.paymentProviderPlanId
) &&
!SubscriptionPlan.isCustomPricingPlan(
props.selectedProject.paymentProviderPlanId
) &&
!isPaymentMethodCountLoading &&
paymentMethodCount === 0 ? (
<Button
title="Add Card Details"
onClick={() => {
Navigation.navigate(
RouteUtil.populateRouteParams(
RouteMap[
PageMap.SETTINGS_BILLING
PageMap.SETTINGS_BILLING
] as Route
)
);
@@ -206,19 +206,21 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
<></>
)}
{BILLING_ENABLED &&
props.selectedProject?.id &&
props.selectedProject.paymentProviderPlanId &&
SubscriptionPlan.isFreePlan(
props.selectedProject.paymentProviderPlanId
) ? (
props.selectedProject?.id &&
props.selectedProject.paymentProviderPlanId &&
SubscriptionPlan.isFreePlan(
props.selectedProject.paymentProviderPlanId
) ? (
<Upgrade projectId={props.selectedProject.id} />
) : (
<></>
)}
<Help />
<UserProfile onClickUserProfle={() => {
setShowProfileModal(true);
}} />
<UserProfile
onClickUserProfle={() => {
setShowProfileModal(true);
}}
/>
</>
}
/>
@@ -243,15 +245,13 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
/>
)}
{
showProfileModal && (
<UserProfileModal
onClose={() => {
setShowProfileModal(false);
}}
/>
)
}
{showProfileModal && (
<UserProfileModal
onClose={() => {
setShowProfileModal(false);
}}
/>
)}
{showActiveIncidentsModal && (
<ActiveIncidentsModal

View File

@@ -19,7 +19,9 @@ const Help: FunctionComponent = (): ReactElement => {
<IconDropdwonItem
title="Chat on Slack"
icon={IconProp.Slack}
url={URL.fromString('https://join.slack.com/t/oneuptimesupport/shared_invite/zt-1kavkds2f-gegm_wePorvwvM3M_SaoCQ')}
url={URL.fromString(
'https://join.slack.com/t/oneuptimesupport/shared_invite/zt-1kavkds2f-gegm_wePorvwvM3M_SaoCQ'
)}
/>
</IconDropdwonRow>
</IconDropdwonMenu>

View File

@@ -11,12 +11,13 @@ import PageMap from '../../Utils/PageMap';
import UserUtil from 'CommonUI/src/Utils/User';
import OneUptimeLogo from 'CommonUI/src/Images/users/blank-profile.svg';
export interface ComponentProps{
onClickUserProfle: ()=> void;
export interface ComponentProps {
onClickUserProfle: () => void;
}
const DashboardUserProfile: FunctionComponent<ComponentProps> = (props: ComponentProps): ReactElement => {
const DashboardUserProfile: FunctionComponent<ComponentProps> = (
props: ComponentProps
): ReactElement => {
return (
<>
<UserProfile
@@ -42,8 +43,6 @@ const DashboardUserProfile: FunctionComponent<ComponentProps> = (props: Componen
/>
</UserProfileMenu>
</UserProfile>
</>
);
};

View File

@@ -17,9 +17,8 @@ export interface ComponentProps {
const UserProfileModal: FunctionComponent<ComponentProps> = (
props: ComponentProps
): ReactElement => {
const [hasPasswordChanged, setHasPasswordChanged] = useState<boolean>(false);
const [hasPasswordChanged, setHasPasswordChanged] =
useState<boolean>(false);
return (
<>
@@ -34,27 +33,24 @@ const UserProfileModal: FunctionComponent<ComponentProps> = (
description: "Here's are some of your details.",
icon: IconProp.User,
}}
isEditable={true}
formFields={[
{
field: {
email: true,
},
fieldType:
FormFieldSchemaType.Email,
placeholder:
'jeff@example.com',
fieldType: FormFieldSchemaType.Email,
placeholder: 'jeff@example.com',
required: true,
title: 'Email',
description: 'You will have to verify your email again if you change it'
description:
'You will have to verify your email again if you change it',
},
{
field: {
name: true,
},
fieldType:
FormFieldSchemaType.Text,
fieldType: FormFieldSchemaType.Text,
placeholder: 'Jeff Smith',
required: true,
title: 'Full Name',
@@ -63,24 +59,20 @@ const UserProfileModal: FunctionComponent<ComponentProps> = (
field: {
companyName: true,
},
fieldType:
FormFieldSchemaType.Text,
fieldType: FormFieldSchemaType.Text,
placeholder: 'Acme, Inc.',
required: true,
title: 'Company Name',
},
{
field: {
companyPhoneNumber:
true,
companyPhoneNumber: true,
},
fieldType:
FormFieldSchemaType.Phone,
fieldType: FormFieldSchemaType.Phone,
required: true,
placeholder:
'+1-123-456-7890',
placeholder: '+1-123-456-7890',
title: 'Phone Number',
}
},
]}
modelDetailProps={{
showDetailsInNumberOfColumns: 2,
@@ -111,14 +103,12 @@ const UserProfileModal: FunctionComponent<ComponentProps> = (
},
title: 'Company Phone Number',
},
],
modelId: UserUtil.getUserId()
modelId: UserUtil.getUserId(),
}}
/>
<div className='flex width-max'>
<div className="flex width-max">
<CardModelDetail<User>
cardProps={{
title: 'Profile Picture',
@@ -135,7 +125,8 @@ const UserProfileModal: FunctionComponent<ComponentProps> = (
title: 'Profile Picture',
fieldType: FormFieldSchemaType.ImageFile,
required: false,
placeholder: 'Please upload your profile picture here.',
placeholder:
'Please upload your profile picture here.',
},
]}
modelDetailProps={{
@@ -155,66 +146,70 @@ const UserProfileModal: FunctionComponent<ComponentProps> = (
placeholder: 'No profile picture uploaded.',
},
],
modelId: UserUtil.getUserId()
modelId: UserUtil.getUserId(),
}}
className="width-half"
/>
<Card style={{marginLeft: "30px"}} className="width-half" title={'Update Password'} description={'You can set a new password here if you wish to do so.'} icon={IconProp.Lock}>
{!hasPasswordChanged ? <ModelForm<User>
modelType={User}
onSuccess={() => {
setHasPasswordChanged(true);
}}
submitButtonStyleType={ButtonStyleType.OUTLINE}
id="change-password-form"
showAsColumns={1}
doNotFetchExistingModel={true}
modelIdToEdit={UserUtil.getUserId()}
maxPrimaryButtonWidth={true}
initialValues={{
password: '',
confirmPassword: '',
}}
fields={[
{
field: {
password: true,
<Card
style={{ marginLeft: '30px' }}
className="width-half"
title={'Update Password'}
description={
'You can set a new password here if you wish to do so.'
}
icon={IconProp.Lock}
>
{!hasPasswordChanged ? (
<ModelForm<User>
modelType={User}
onSuccess={() => {
setHasPasswordChanged(true);
}}
submitButtonStyleType={ButtonStyleType.OUTLINE}
id="change-password-form"
showAsColumns={1}
doNotFetchExistingModel={true}
modelIdToEdit={UserUtil.getUserId()}
maxPrimaryButtonWidth={true}
initialValues={{
password: '',
confirmPassword: '',
}}
fields={[
{
field: {
password: true,
},
fieldType: FormFieldSchemaType.Password,
validation: {
minLength: 6,
},
placeholder: 'Password',
title: 'Password',
required: true,
},
fieldType:
FormFieldSchemaType.Password,
validation: {
minLength: 6,
{
field: {
password: true,
},
validation: {
minLength: 6,
toMatchField: 'password',
},
fieldType: FormFieldSchemaType.Password,
placeholder: 'Confirm Password',
title: 'Confirm Password',
overideFieldKey: 'confirmPassword',
required: true,
},
placeholder: 'Password',
title: 'Password',
required: true,
},
{
field: {
password: true,
},
validation: {
minLength: 6,
toMatchField:
'password',
},
fieldType:
FormFieldSchemaType.Password,
placeholder:
'Confirm Password',
title: 'Confirm Password',
overideFieldKey:
'confirmPassword',
required: true,
},
]}
formType={FormType.Update}
submitButtonText={'Update Password'}
/> : <p>Your password has been updated.</p>}
]}
formType={FormType.Update}
submitButtonText={'Update Password'}
/>
) : (
<p>Your password has been updated.</p>
)}
</Card>
</div>
</FullPageModal>

View File

@@ -189,7 +189,8 @@ const Settings: FunctionComponent<ComponentProps> = (
<></>
)}
{item['status'] !== 'paid' && item['status'] !== 'draft' ? (
{item['status'] !== 'paid' &&
item['status'] !== 'draft' ? (
<Button
icon={IconProp.Billing}
onClick={async () => {

View File

@@ -72,8 +72,7 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
cardProps={{
icon: IconProp.Globe,
title: 'Custom Domains',
description:
`Important: Please add ${StatusPageCNameRecord} as your CNAME for these domains for this to work.`,
description: `Important: Please add ${StatusPageCNameRecord} as your CNAME for these domains for this to work.`,
}}
onBeforeCreate={(
item: StatusPageDomain
@@ -138,10 +137,9 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
tooltipText: (item: StatusPageDomain): string => {
if (item['isCnameVerified']) {
return 'We have verified your CNAME record.';
} else {
return `Please add a new CNAME record to your domain ${item['fullDomain']}. It should look like CNAME ${item['fullDomain']} ${StatusPageCNameRecord}`;
}
}
return `Please add a new CNAME record to your domain ${item['fullDomain']}. It should look like CNAME ${item['fullDomain']} ${StatusPageCNameRecord}`;
},
},
{
field: {
@@ -152,7 +150,7 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
isFilterable: true,
tooltipText: (_item: StatusPageDomain): string => {
return 'This will happen automatically after CNAME is verified. Please allow 24 hours for SSL to be provisioned after CNAME is verified. If it does not happen in 24 hours, please contact support.';
}
},
},
]}
/>

View File

@@ -414,7 +414,7 @@ router.post(
email: true,
isMasterAdmin: true,
isEmailVerified: true,
profilePictureId: true
profilePictureId: true,
},
props: {
isRoot: true,

View File

@@ -1,4 +1,4 @@
import { Column, Entity, Index} from 'typeorm';
import { Column, Entity, Index } from 'typeorm';
import BaseModel from 'Common/Models/BaseModel';
import TableColumnType from 'Common/Types/Database/TableColumnType';
import TableColumn from 'Common/Types/Database/TableColumn';
@@ -19,7 +19,6 @@ import SingularPluralName from 'Common/Types/Database/SingularPluralName';
name: 'GreenlockChallenge',
})
export default class GreenlockCertificate extends BaseModel {
@Index()
@ColumnAccessControl({
create: [],
@@ -35,7 +34,6 @@ export default class GreenlockCertificate extends BaseModel {
})
public key?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],

View File

@@ -1,4 +1,4 @@
import { Column, Entity, Index} from 'typeorm';
import { Column, Entity, Index } from 'typeorm';
import BaseModel from 'Common/Models/BaseModel';
import TableColumnType from 'Common/Types/Database/TableColumnType';
import TableColumn from 'Common/Types/Database/TableColumn';
@@ -19,7 +19,6 @@ import SingularPluralName from 'Common/Types/Database/SingularPluralName';
name: 'GreenlockChallenge',
})
export default class GreenlockChallenge extends BaseModel {
@Index()
@ColumnAccessControl({
create: [],
@@ -35,7 +34,6 @@ export default class GreenlockChallenge extends BaseModel {
})
public key?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],

View File

@@ -60,7 +60,7 @@ import File from './File';
import BillingInvoice from './BillingInvoice';
// Greenlock
import GreenlockChallenge from './GreenlockChallenge'
import GreenlockChallenge from './GreenlockChallenge';
import GreenlockCertificate from './GreenlockCertificate';
export default [
@@ -108,5 +108,5 @@ export default [
BillingInvoice,
GreenlockChallenge,
GreenlockCertificate
GreenlockCertificate,
];

View File

@@ -239,7 +239,6 @@ export default class StatusPageDomain extends BaseModel {
@JoinColumn({ name: 'deletedByUserId' })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
@@ -253,10 +252,9 @@ export default class StatusPageDomain extends BaseModel {
})
public greenlockConfig?: JSON = undefined;
// This token is used by the Worker.
// This token is used by the Worker.
// worker pings the status page of customers - eg: status.company.com/verify-token/:id
// and the end point on Sttaus Page proejct returns 200.
// and the end point on Sttaus Page proejct returns 200.
// when that happens the isCnameVerified is set to True and the certificate is added to greenlock.
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.CanCreateStatusPageDomain],

View File

@@ -228,7 +228,6 @@ class User extends UserModel {
})
public companyPhoneNumber?: Phone = undefined;
@ColumnAccessControl({
create: [],
read: [
@@ -276,7 +275,6 @@ class User extends UserModel {
})
public profilePictureId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -67,6 +67,8 @@ RUN npm install
# Expose ports.
# - 3105: StatusPage
EXPOSE 3105
# API
EXPOSE 3106
{{ if eq .Env.ENVIRONMENT "development" }}
#Run the app

View File

@@ -6,84 +6,95 @@ import Express, {
} from 'CommonServer/Utils/Express';
import logger from 'CommonServer/Utils/Logger';
import { PostgresAppInstance } from 'CommonServer/Infrastructure/PostgresDatabase';
import GreenlockChallengeService from "CommonServer/Services/GreenlockChallengeService";
import GreenlockChallengeService from 'CommonServer/Services/GreenlockChallengeService';
import GreenlockChallenge from 'Model/Models/GreenlockChallenge';
import Response from 'CommonServer/Utils/Response';
import NotFoundException from 'Common/Types/Exception/NotFoundException';
import BadDataException from 'Common/Types/Exception/BadDataException';
import StatusPageDomain from 'Model/Models/StatusPageDomain';
import StatusPageDomainService from "CommonServer/Services/StatusPageDomainService";
import StatusPageDomainService from 'CommonServer/Services/StatusPageDomainService';
export const APP_NAME: string = 'status-page';
const app: ExpressApplication = Express.getExpressApp();
// ACME Challenge Validation.
app.get('/.well-known/acme-challenge/:token', async (
req: ExpressRequest,
res: ExpressResponse
) => {
const challenge : GreenlockChallenge | null = await GreenlockChallengeService.findOneBy({
query: {
key: req.params['token'] as string
},
select: {
challenge: true,
},
props: {
isRoot: true,
// ACME Challenge Validation.
app.get(
'/.well-known/acme-challenge/:token',
async (req: ExpressRequest, res: ExpressResponse) => {
const challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: req.params['token'] as string,
},
select: {
challenge: true,
},
props: {
isRoot: true,
},
});
if (!challenge) {
return Response.sendErrorResponse(
req,
res,
new NotFoundException('Challenge not found')
);
}
})
if (!challenge) {
return Response.sendErrorResponse(req, res, new NotFoundException("Challenge not found"));
return Response.sendTextResponse(
req,
res,
challenge.challenge as string
);
}
);
return Response.sendTextResponse(req, res, challenge.challenge as string);
});
app.get(
'/status-page-api/cname-verification/:token',
async (req: ExpressRequest, res: ExpressResponse) => {
const host: string | undefined = req.get('host');
app.get('/status-page-api/cname-verification/:token', async (
req: ExpressRequest,
res: ExpressResponse
) => {
const host: string | undefined = req.get('host');
if (!host) {
throw new BadDataException("Host not found");
}
const domain : StatusPageDomain | null = await StatusPageDomainService.findOneBy({
query: {
cnameVerificationToken: req.params['token'] as string,
fullDomain: host
},
select: {
_id: true
},
props: {
isRoot: true,
if (!host) {
throw new BadDataException('Host not found');
}
})
if (!domain) {
return Response.sendErrorResponse(req, res, new BadDataException("Invalid token."));
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
query: {
cnameVerificationToken: req.params['token'] as string,
fullDomain: host,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
if (!domain) {
return Response.sendErrorResponse(
req,
res,
new BadDataException('Invalid token.')
);
}
return Response.sendEmptyResponse(req, res);
}
return Response.sendEmptyResponse(req, res);
});
);
const init: Function = async (): Promise<void> => {
try {
// init the app
await App(APP_NAME);
// connect to the database.
await PostgresAppInstance.connect(
// connect to the database.
await PostgresAppInstance.connect(
PostgresAppInstance.getDatasourceOptions()
);
} catch (err) {
logger.error('App Init Failed:');
logger.error(err);

View File

@@ -20,12 +20,12 @@
"use-async-effect": "^2.2.6"
},
"scripts": {
"dev": "webpack-dev-server --port=3105 --mode=development & nodemon &",
"dev": "webpack-dev-server --port=3105 --mode=development & nodemon",
"build": "webpack build --mode=production",
"test": "react-app-rewired test",
"eject": "webpack eject",
"compile": "tsc",
"start": "node --require ts-node/register Index.ts & http-server ./public -p 3105",
"start": "http-server ./public -p 3105 & node --require ts-node/register Index.ts",
"audit": "npm audit --audit-level=low",
"preinstall": "npx npm-force-resolutions || echo 'No package-lock.json file. Skipping force resolutions'",
"dep-check": "depcheck ./ --skip-missing=true'"

View File

@@ -15,7 +15,7 @@ import { DASHBOARD_URL } from 'CommonUI/src/Config';
const LoginPage: FunctionComponent = () => {
const apiUrl: URL = LOGIN_API_URL;
if (UserUtil.isLoggedIn()) {
Navigation.navigate(DASHBOARD_URL);
}

View File

@@ -2,15 +2,14 @@ import Route from 'Common/Types/API/Route';
import URL from 'Common/Types/API/URL';
import { DASHBOARD_API_URL } from 'CommonUI/src/Config';
export const LOGIN_API_URL: URL = URL.fromURL(DASHBOARD_API_URL).addRoute(
new Route('/status-page-private-user/login')
);
export const FORGOT_PASSWORD_API_URL: URL = URL.fromURL(DASHBOARD_API_URL).addRoute(
new Route('/status-page-private-user/forgot-password')
);
export const FORGOT_PASSWORD_API_URL: URL = URL.fromURL(
DASHBOARD_API_URL
).addRoute(new Route('/status-page-private-user/forgot-password'));
export const RESET_PASSWORD_API_URL: URL = URL.fromURL(DASHBOARD_API_URL).addRoute(
new Route('/status-page-private-user/reset-password')
);
export const RESET_PASSWORD_API_URL: URL = URL.fromURL(
DASHBOARD_API_URL
).addRoute(new Route('/status-page-private-user/reset-password'));

View File

@@ -9,13 +9,13 @@ import './Jobs/PaymentProvider/CheckSubscriptionStatus';
// Certs Routers
import StausPageCerts from './Jobs/StatusPageCerts/StausPageCerts';
import Express , { ExpressApplication }from 'CommonServer/Utils/Express';
import Express, { ExpressApplication } from 'CommonServer/Utils/Express';
const APP_NAME: string = 'workers';
const app: ExpressApplication = Express.getExpressApp();
//cert routes.
//cert routes.
app.use(StausPageCerts);
const init: Function = async (): Promise<void> => {

View File

@@ -18,36 +18,39 @@ import ClusterKeyAuthorization from 'CommonServer/Middleware/ClusterKeyAuthoriza
import { JSONObject } from 'Common/Types/JSON';
import Response from 'CommonServer/Utils/Response';
import LIMIT_MAX from 'Common/Types/Database/LimitMax';
import http from 'http';
const router: ExpressRouter = Express.getRouter();
const greenlock = Greenlock.create({
const greenlock: any = Greenlock.create({
configFile: '/greenlockrc',
packageRoot: `/usr/src/app`,
manager: '/usr/src/app/Utils/Greenlock/Manager.ts',
manager: '/usr/src/app/Utils/Greenlock/Manager.ts',
approveDomains: async (opts: any) => {
const domain: StatusPageDomain | null = await StatusPageDomainService.findOneBy({
query: {
fullDomain: opts.domain,
},
select: {
_id: true,
fullDomain: true
},
props: {
isRoot: true,
}
});
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
query: {
fullDomain: opts.domain,
},
select: {
_id: true,
fullDomain: true,
},
props: {
isRoot: true,
},
});
if (!domain) {
throw new BadDataException(`Domain ${opts.domain} does not exist in StatusPageDomain`);
throw new BadDataException(
`Domain ${opts.domain} does not exist in StatusPageDomain`
);
}
return opts; // or Promise.resolve(opts);
},
store: {
module: '/usr/src/app/Utils/Greenlock/Store.ts'
module: '/usr/src/app/Utils/Greenlock/Store.ts',
},
// Staging for testing environments
staging: IsDevelopment,
@@ -57,7 +60,7 @@ const greenlock = Greenlock.create({
maintainerEmail: 'lets-encrypt@oneuptime.com',
// for an RFC 8555 / RFC 7231 ACME client user agent
packageAgent: "oneuptime/1.0.0",
packageAgent: 'oneuptime/1.0.0',
notify: function (event: string, details: any) {
if ('error' === event) {
logger.error(details);
@@ -67,30 +70,25 @@ const greenlock = Greenlock.create({
agreeToTerms: true,
challenges: {
'http-01': {
module: '/usr/src/app/Utils/Greenlock/HttpChallenge.ts'
}
module: '/usr/src/app/Utils/Greenlock/HttpChallenge.ts',
},
},
});
// Delete
router.delete(
`/certs`,
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction
) => {
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
if (!body['domain']) {
throw new BadDataException("Domain is required");
throw new BadDataException('Domain is required');
}
await greenlock.remove({
subject: body['domain']
subject: body['domain'],
});
return Response.sendEmptyResponse(req, res);
@@ -100,25 +98,20 @@ router.delete(
}
);
// Create
router.post(
`/certs`,
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction
) => {
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
if (!body['domain']) {
throw new BadDataException("Domain is required");
throw new BadDataException('Domain is required');
}
await greenlock.add({
subject: body['domain']
subject: body['domain'],
});
return Response.sendEmptyResponse(req, res);
@@ -132,19 +125,17 @@ router.post(
router.get(
`/certs`,
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction
) => {
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
if (!body['domain']) {
throw new BadDataException("Domain is required");
throw new BadDataException('Domain is required');
}
const site = await greenlock.get({ servername: body['domain'] as string });
const site: JSONObject = await greenlock.get({
servername: body['domain'] as string,
});
return Response.sendJsonObjectResponse(req, res, site);
} catch (err) {
@@ -153,149 +144,174 @@ router.get(
}
);
RunCron(
'StatusPageCerts:Renew',
IsDevelopment ? EVERY_MINUTE : EVERY_HOUR,
async () => {
logger.info('Start');
// fetch all domains wiht expired certs.
await greenlock.renew({});
logger.info('End');
}
);
RunCron('StatusPageCerts:Renew', IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, async () => {
logger.info("Start");
// fetch all domains wiht expired certs.
await greenlock.renew({});
logger.info("End");
});
RunCron('StatusPageCerts:AddCerts', IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, async () => {
const domains: Array<StatusPageDomain> = await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: false,
},
select: {
_id: true,
fullDomain: true,
cnameVerificationToken: true
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
}
});
for (const domain of domains) {
logger.info(`StatusPageCerts:AddCerts - Checking CNAME ${domain.fullDomain}`);
// Check CNAME validation and if that fails. Remove certs from Greenlock.
const isValid = await checkCnameValidation(domain.fullDomain!, domain.cnameVerificationToken!);
if (isValid) {
logger.info(`StatusPageCerts:AddCerts - CNAME for ${domain.fullDomain} is valid. Adding domain to greenlock.`);
await greenlock.add({
subject: domain.fullDomain
RunCron(
'StatusPageCerts:AddCerts',
IsDevelopment ? EVERY_MINUTE : EVERY_HOUR,
async () => {
const domains: Array<StatusPageDomain> =
await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: false,
},
select: {
_id: true,
fullDomain: true,
cnameVerificationToken: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isAddedtoGreenlock: true,
isCnameVerified: true
},
props: {
isRoot: true
}
})
for (const domain of domains) {
logger.info(
`StatusPageCerts:AddCerts - Checking CNAME ${domain.fullDomain}`
);
// Check CNAME validation and if that fails. Remove certs from Greenlock.
const isValid: boolean = await checkCnameValidation(
domain.fullDomain!,
domain.cnameVerificationToken!
);
logger.info(`StatusPageCerts:AddCerts - ${domain.fullDomain} added to greenlock.`);
if (isValid) {
logger.info(
`StatusPageCerts:AddCerts - CNAME for ${domain.fullDomain} is valid. Adding domain to greenlock.`
);
} else {
logger.info(`StatusPageCerts:AddCerts - CNAME for ${domain.fullDomain} is invalid. Removing cert`);
await greenlock.add({
subject: domain.fullDomain,
});
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isAddedtoGreenlock: true,
isCnameVerified: true,
},
props: {
isRoot: true,
},
});
logger.info(
`StatusPageCerts:AddCerts - ${domain.fullDomain} added to greenlock.`
);
} else {
logger.info(
`StatusPageCerts:AddCerts - CNAME for ${domain.fullDomain} is invalid. Removing cert`
);
}
}
}
});
);
RunCron(
'StatusPageCerts:RemoveCerts',
IsDevelopment ? EVERY_MINUTE : EVERY_HOUR,
async () => {
// Fetch all domains where certs are added to greenlock.
RunCron('StatusPageCerts:RemoveCerts', IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, async () => {
// Fetch all domains where certs are added to greenlock.
const domains: Array<StatusPageDomain> = await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: true,
},
select: {
_id: true,
fullDomain: true,
cnameVerificationToken: true
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
}
});
for (const domain of domains) {
logger.info(`StatusPageCerts:RemoveCerts - Checking CNAME ${domain.fullDomain}`);
// Check CNAME validation and if that fails. Remove certs from Greenlock.
const isValid = await checkCnameValidation(domain.fullDomain!, domain.cnameVerificationToken!);
if (!isValid) {
logger.info(`StatusPageCerts:RemoveCerts - CNAME for ${domain.fullDomain} is invalid. Removing domain from greenlock.`);
await greenlock.remove({
subject: domain.fullDomain
const domains: Array<StatusPageDomain> =
await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: true,
},
select: {
_id: true,
fullDomain: true,
cnameVerificationToken: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isAddedtoGreenlock: false,
isCnameVerified: false
},
props: {
isRoot: true
}
})
for (const domain of domains) {
logger.info(
`StatusPageCerts:RemoveCerts - Checking CNAME ${domain.fullDomain}`
);
logger.info(`StatusPageCerts:RemoveCerts - ${domain.fullDomain} removed from greenlock.`);
// Check CNAME validation and if that fails. Remove certs from Greenlock.
const isValid: boolean = await checkCnameValidation(
domain.fullDomain!,
domain.cnameVerificationToken!
);
} else {
logger.info(`StatusPageCerts:RemoveCerts - CNAME for ${domain.fullDomain} is valid`);
if (!isValid) {
logger.info(
`StatusPageCerts:RemoveCerts - CNAME for ${domain.fullDomain} is invalid. Removing domain from greenlock.`
);
await greenlock.remove({
subject: domain.fullDomain,
});
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isAddedtoGreenlock: false,
isCnameVerified: false,
},
props: {
isRoot: true,
},
});
logger.info(
`StatusPageCerts:RemoveCerts - ${domain.fullDomain} removed from greenlock.`
);
} else {
logger.info(
`StatusPageCerts:RemoveCerts - CNAME for ${domain.fullDomain} is valid`
);
}
}
}
});
);
const checkCnameValidation = (fulldomain: string, token: string): Promise<boolean> => {
return new Promise((resolve, reject) => {
const checkCnameValidation: Function = (
fulldomain: string,
token: string
): Promise<boolean> => {
return new Promise((resolve: Function, reject: Function) => {
try {
https.request({
host: fulldomain,
port: 443,
path: '/status-page-api/cname-verification/' + token,
method: 'GET',
rejectUnauthorized: false,
agent: false
}, (res) => {
if (res.statusCode === 200) {
return resolve(true);
https.request(
{
host: fulldomain,
port: 443,
path: '/status-page-api/cname-verification/' + token,
method: 'GET',
rejectUnauthorized: false,
agent: false,
},
(res: http.IncomingMessage) => {
if (res.statusCode === 200) {
return resolve(true);
}
return resolve(false);
}
return resolve(false);
});
);
} catch (err) {
reject(err);
}
})
});
};
}
export default router;
export default router;

View File

@@ -1,4 +1,3 @@
export const EVERY_MINUTE: string = '* * * * *';
export const EVERY_DAY: string = '0 8 * * *';
export const EVERY_HOUR: string = '1 * * * *';

View File

@@ -1,30 +1,29 @@
import GreenlockChallenge from "Model/Models/GreenlockChallenge";
import GreenlockChallengeService from "CommonServer/Services/GreenlockChallengeService";
import GreenlockChallenge from 'Model/Models/GreenlockChallenge';
import GreenlockChallengeService from 'CommonServer/Services/GreenlockChallengeService';
// because greenlock package expects module.exports.
// because greenlock package expects module.exports.
module.exports = {
init: async (): Promise<null> => {
return Promise.resolve(null);
},
set: async (data: any): Promise<null> => {
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
var ch: any = data.challenge;
var key: string = ch.identifier.value + '#' + ch.token;
let challenge: GreenlockChallenge | null = await GreenlockChallengeService.findOneBy({
query: {
key: key
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true
}
});
let challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: key,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!challenge) {
challenge = new GreenlockChallenge();
@@ -34,66 +33,62 @@ module.exports = {
await GreenlockChallengeService.create({
data: challenge,
props: {
isRoot: true
}
isRoot: true,
},
});
} else {
challenge.challenge = ch.keyAuthorization;
await GreenlockChallengeService.updateOneById({
id: challenge.id!,
data: challenge,
props: {
isRoot: true
}
})
isRoot: true,
},
});
}
//
//
return null;
},
get: async (data: any): Promise<null | any> => {
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
var ch: any = data.challenge;
var key: string = ch.identifier.value + '#' + ch.token;
let challenge: GreenlockChallenge | null = await GreenlockChallengeService.findOneBy({
query: {
key: key
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true
}
});
const challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: key,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!challenge) {
return null;
}
return { keyAuthorization: challenge.challenge } ;
return { keyAuthorization: challenge.challenge };
},
remove: async (data: any): Promise<null> => {
var ch = data.challenge;
var key = ch.identifier.value + '#' + ch.token;
await GreenlockChallengeService.deleteOneBy({
query: {
key: key
},
props: {
isRoot: true,
ignoreHooks: true
}
});
return null;
}
}
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
await GreenlockChallengeService.deleteOneBy({
query: {
key: key,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
return null;
},
};

View File

@@ -1,82 +1,84 @@
// Docs: https://git.rootprojects.org/root/greenlock-manager.js
import StatusPageDomainService from "CommonServer/Services/StatusPageDomainService";
import StatusPageDomain from "Model/Models/StatusPageDomain";
import StatusPageDomainService from 'CommonServer/Services/StatusPageDomainService';
import StatusPageDomain from 'Model/Models/StatusPageDomain';
// because greenlock package expects module.exports.
// because greenlock package expects module.exports.
module.exports = {
create: () => {
console.log("Manager created");
return {
// Get
get: async ({ servername }: { servername: string }): Promise<JSON | undefined> => {
get: async ({
servername,
}: {
servername: string;
}): Promise<JSON | undefined> => {
// Required: find the certificate with the subject of `servername`
// Optional (multi-domain certs support): find a certificate with `servername` as an altname
// Optional (wildcard support): find a certificate with `wildname` as an altname
// { subject, altnames, renewAt, deletedAt, challenges, ... }
const domain: StatusPageDomain | null = await StatusPageDomainService.findOneBy({
query: {
fullDomain: servername,
},
props: {
isRoot: true,
ignoreHooks: true
},
select: {
_id: true,
greenlockConfig: true,
}
});
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
query: {
fullDomain: servername,
},
props: {
isRoot: true,
ignoreHooks: true,
},
select: {
_id: true,
greenlockConfig: true,
},
});
if (!domain || !domain.greenlockConfig) {
return undefined;
}
return domain.greenlockConfig;
},
// Set
set: async (opts: any) => {
set: async (opts: any) => {
// { subject, altnames, renewAt, deletedAt }
// Required: updated `renewAt` and `deletedAt` for certificate matching `subject`
if(!opts.subject){
return
if (!opts.subject) {
return;
}
const domain: StatusPageDomain | null = await StatusPageDomainService.findOneBy({
query: {
fullDomain: opts['subject'] as string,
},
props: {
isRoot: true,
},
select: {
_id: true,
greenlockConfig: true,
}
});
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
query: {
fullDomain: opts['subject'] as string,
},
props: {
isRoot: true,
},
select: {
_id: true,
greenlockConfig: true,
},
});
if (!domain) {
return;
return;
}
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
greenlockConfig: opts
greenlockConfig: opts,
},
props: {
isRoot: true,
ignoreHooks: true
}
ignoreHooks: true,
},
});
}
},
};
}
}
},
};

View File

@@ -4,102 +4,106 @@ import GreenlockCertificate from 'Model/Models/GreenlockCertificate';
import GreenlockCertificateService from 'CommonServer/Services/GreenlockCertificateService';
module.exports = {
create: () => {
const saveCertificate = async (id: string, blob: string): Promise<null> => {
let cert: GreenlockCertificate | null = await GreenlockCertificateService.findOneBy({
query: {
key: id
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true
}
});
const saveCertificate: Function = async (
id: string,
blob: string
): Promise<null> => {
let cert: GreenlockCertificate | null =
await GreenlockCertificateService.findOneBy({
query: {
key: id,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!cert) {
cert = new GreenlockCertificate();
cert.key = id;
cert.blob = blob;
await GreenlockCertificateService.create({
data: cert,
props: {
isRoot: true
}
isRoot: true,
},
});
} else {
cert.blob = blob;
await GreenlockCertificateService.updateOneById({
id: cert.id!,
data: cert,
props: {
isRoot: true
}
})
isRoot: true,
},
});
}
//
//
return null;
}
const getCertificate = async (id: string): Promise<null | string> => {
let cert: GreenlockCertificate | null = await GreenlockCertificateService.findOneBy({
query: {
key: id
},
select: {
_id: true,
blob: true,
},
props: {
isRoot: true,
ignoreHooks: true
}
});
};
const getCertificate: Function = async (id: string): Promise<null | string> => {
const cert: GreenlockCertificate | null =
await GreenlockCertificateService.findOneBy({
query: {
key: id,
},
select: {
_id: true,
blob: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!cert || !cert.blob) {
return null;
} else {
return cert.blob;
}
}
const saveKeypair = async (id: string, blob: string): Promise<null> => {
return await saveCertificate(id, blob);
}
return cert.blob;
};
const getKeypair = async (id: string): Promise<null | string> => {
const saveKeypair: Function = async (id: string, blob: string): Promise<null> => {
return await saveCertificate(id, blob);
};
const getKeypair: Function = async (id: string): Promise<null | string> => {
return await getCertificate(id);
}
};
return {
accounts: {
// Whenever a new keypair is used to successfully create an account, we need to save its keypair
setKeypair: async (opts: any): Promise<null> => {
const id: string = opts.account.id || opts.email || 'default';
const keypair: any = opts.keypair;
var id = opts.account.id || opts.email || 'default';
var keypair = opts.keypair;
return await saveKeypair(id, JSON.stringify({
privateKeyPem: keypair.privateKeyPem // string PEM
, privateKeyJwk: keypair.privateKeyJwk // object JWK
})); // Must return or Promise `null` instead of `undefined`
return await saveKeypair(
id,
JSON.stringify({
privateKeyPem: keypair.privateKeyPem, // string PEM
privateKeyJwk: keypair.privateKeyJwk, // object JWK
})
); // Must return or Promise `null` instead of `undefined`
},
// We need a way to retrieve a prior account's keypair for renewals and additional ACME certificate "orders"
checkKeypair: async (opts: any): Promise<any | null> => {
console.log('accounts.checkKeypair:', opts.account, opts.email);
var id = opts.account.id || opts.email || 'default';
var keyblob = await getKeypair(id);
const id: string = opts.account.id || opts.email || 'default';
const keyblob: any = await getKeypair(id);
if (!keyblob) { return null; }
if (!keyblob) {
return null;
}
return JSON.parse(keyblob);
},
@@ -107,72 +111,77 @@ module.exports = {
certificate: {
setKeypair: async (opts: any): Promise<null> => {
// The ID is a string that doesn't clash between accounts and certificates.
// That's all you need to know... unless you're doing something special (in which case you're on your own).
var id = opts.certificate.kid || opts.certificate.id || opts.subject;
var keypair = opts.keypair;
return await saveKeypair(id, JSON.stringify({
privateKeyPem: keypair.privateKeyPem // string PEM
, privateKeyJwk: keypair.privateKeyJwk // object JWK
})); // Must return or Promise `null` instead of `undefined`
const id: string =
opts.certificate.kid ||
opts.certificate.id ||
opts.subject;
const keypair: any = opts.keypair;
return await saveKeypair(
id,
JSON.stringify({
privateKeyPem: keypair.privateKeyPem, // string PEM
privateKeyJwk: keypair.privateKeyJwk, // object JWK
})
); // Must return or Promise `null` instead of `undefined`
// Side Note: you can use the "keypairs" package to convert between
// public and private for jwk and pem, as well as convert JWK <-> PEM
},
// You won't be able to use a certificate without it's private key, gotta save it
checkKeypair: async (opts: any): Promise<any | null> => {
var id = opts.certificate.kid || opts.certificate.id || opts.subject;
var keyblob = await getKeypair(id);
if (!keyblob) { return null; }
const id =
opts.certificate.kid ||
opts.certificate.id ||
opts.subject;
const keyblob = await getKeypair(id);
if (!keyblob) {
return null;
}
return JSON.parse(keyblob);
},
// And you'll also need to save certificates. You may find the metadata useful to save
// (perhaps to delete expired keys), but the same information can also be redireved from
// the key using the "cert-info" package.
set: async (opts: any): Promise<null> => {
var id = opts.certificate.id || opts.subject;
var pems = opts.pems;
return await saveCertificate(id, JSON.stringify({
cert: pems.cert // string PEM
, chain: pems.chain // string PEM
, subject: pems.subject // string name 'example.com
, altnames: pems.altnames // Array of string names [ 'example.com', '*.example.com', 'foo.bar.example.com' ]
, issuedAt: pems.issuedAt // date number in ms (a.k.a. NotBefore)
, expiresAt: pems.expiresAt // date number in ms (a.k.a. NotAfter)
})); // Must return or Promise `null` instead of `undefined`
const id = opts.certificate.id || opts.subject;
const pems = opts.pems;
return await saveCertificate(
id,
JSON.stringify({
cert: pems.cert, // string PEM
chain: pems.chain, // string PEM
subject: pems.subject, // string name 'example.com
altnames: pems.altnames, // Array of string names [ 'example.com', '*.example.com', 'foo.bar.example.com' ]
issuedAt: pems.issuedAt, // date number in ms (a.k.a. NotBefore)
expiresAt: pems.expiresAt, // date number in ms (a.k.a. NotAfter)
})
); // Must return or Promise `null` instead of `undefined`
},
// This is actually the first thing to be called after approveDomins(),
// but it's easiest to implement last since it's not useful until there
// are certs that can actually be loaded from storage.
check: async (opts: any): Promise<null | any> => {
var id = opts.certificate.id || opts.subject;
var certblob = await getCertificate(id);
if (!certblob) { return null; }
const id = opts.certificate.id || opts.subject;
const certblob = await getCertificate(id);
if (!certblob) {
return null;
}
return JSON.parse(certblob);
}
},
},
options: {
}
options: {},
};
}
}
},
};