mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Add remaining audit log events
Summary: Adds the remaining batch of audit log events, and a CLI utility to generate documentation for installation and site audit events. Test Plan: Manual. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4356
This commit is contained in:
parent
1927c87413
commit
bda7935714
@ -1,3 +1,6 @@
|
|||||||
|
import {BasicRole, NonGuestRole} from 'app/common/roles';
|
||||||
|
import {StringUnion} from 'app/common/StringUnion';
|
||||||
|
|
||||||
export interface AuditEvent<Name extends AuditEventName> {
|
export interface AuditEvent<Name extends AuditEventName> {
|
||||||
/**
|
/**
|
||||||
* The event.
|
* The event.
|
||||||
@ -12,35 +15,73 @@ export interface AuditEvent<Name extends AuditEventName> {
|
|||||||
*/
|
*/
|
||||||
user: AuditEventUser;
|
user: AuditEventUser;
|
||||||
/**
|
/**
|
||||||
* The event details.
|
* Event-specific details (e.g. IDs of affected resources).
|
||||||
*/
|
*/
|
||||||
details: AuditEventDetails[Name] | {};
|
details: AuditEventDetails[Name] | {};
|
||||||
/**
|
/**
|
||||||
* The context of the event.
|
* The context that the event occurred in (e.g. workspace, document).
|
||||||
*/
|
*/
|
||||||
context: AuditEventContext;
|
context: AuditEventContext;
|
||||||
/**
|
/**
|
||||||
* The source of the event.
|
* Information about the source of the event (e.g. IP address).
|
||||||
*/
|
*/
|
||||||
source: AuditEventSource;
|
source: AuditEventSource;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* ISO 8601 timestamp of when the event occurred.
|
* ISO 8601 timestamp (e.g. `2024-09-04T14:54:50Z`) of when the event occurred.
|
||||||
*/
|
*/
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuditEventName =
|
export const SiteAuditEventName = StringUnion(
|
||||||
| 'createDocument'
|
'createDocument',
|
||||||
| 'moveDocument'
|
'sendToGoogleDrive',
|
||||||
| 'removeDocument'
|
'renameDocument',
|
||||||
| 'deleteDocument'
|
'pinDocument',
|
||||||
| 'restoreDocumentFromTrash'
|
'unpinDocument',
|
||||||
| 'runSQLQuery';
|
'moveDocument',
|
||||||
|
'removeDocument',
|
||||||
|
'deleteDocument',
|
||||||
|
'restoreDocumentFromTrash',
|
||||||
|
'changeDocumentAccess',
|
||||||
|
'openDocument',
|
||||||
|
'duplicateDocument',
|
||||||
|
'forkDocument',
|
||||||
|
'replaceDocument',
|
||||||
|
'reloadDocument',
|
||||||
|
'truncateDocumentHistory',
|
||||||
|
'deliverWebhookEvents',
|
||||||
|
'clearWebhookQueue',
|
||||||
|
'clearAllWebhookQueues',
|
||||||
|
'runSQLQuery',
|
||||||
|
'createWorkspace',
|
||||||
|
'renameWorkspace',
|
||||||
|
'removeWorkspace',
|
||||||
|
'deleteWorkspace',
|
||||||
|
'restoreWorkspaceFromTrash',
|
||||||
|
'changeWorkspaceAccess',
|
||||||
|
'renameSite',
|
||||||
|
'changeSiteAccess',
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SiteAuditEventName = typeof SiteAuditEventName.type;
|
||||||
|
|
||||||
|
export const AuditEventName = StringUnion(
|
||||||
|
...SiteAuditEventName.values,
|
||||||
|
'createSite',
|
||||||
|
'deleteSite',
|
||||||
|
'changeUserName',
|
||||||
|
'createUserAPIKey',
|
||||||
|
'deleteUserAPIKey',
|
||||||
|
'deleteUser',
|
||||||
|
);
|
||||||
|
|
||||||
|
export type AuditEventName = typeof AuditEventName.type;
|
||||||
|
|
||||||
export type AuditEventUser =
|
export type AuditEventUser =
|
||||||
| User
|
| User
|
||||||
| Anonymous
|
| Anonymous
|
||||||
|
| System
|
||||||
| Unknown;
|
| Unknown;
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@ -54,14 +95,15 @@ interface Anonymous {
|
|||||||
type: 'anonymous';
|
type: 'anonymous';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface System {
|
||||||
|
type: 'system';
|
||||||
|
}
|
||||||
|
|
||||||
interface Unknown {
|
interface Unknown {
|
||||||
type: 'unknown';
|
type: 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuditEventDetails {
|
export interface AuditEventDetails {
|
||||||
/**
|
|
||||||
* A new document was created.
|
|
||||||
*/
|
|
||||||
createDocument: {
|
createDocument: {
|
||||||
/**
|
/**
|
||||||
* The ID of the document.
|
* The ID of the document.
|
||||||
@ -72,22 +114,55 @@ export interface AuditEventDetails {
|
|||||||
*/
|
*/
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
|
sendToGoogleDrive: {
|
||||||
/**
|
/**
|
||||||
* A document was moved to a new workspace.
|
* The ID of the document.
|
||||||
*/
|
*/
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
renameDocument: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The previous name of the document.
|
||||||
|
*/
|
||||||
|
previousName: string;
|
||||||
|
/**
|
||||||
|
* The current name of the document.
|
||||||
|
*/
|
||||||
|
currentName: string;
|
||||||
|
};
|
||||||
|
pinDocument: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The name of the document.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
unpinDocument: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The name of the document.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
moveDocument: {
|
moveDocument: {
|
||||||
/**
|
/**
|
||||||
* The ID of the document.
|
* The ID of the document.
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
/**
|
|
||||||
* The previous workspace.
|
|
||||||
*/
|
|
||||||
previous: {
|
|
||||||
/**
|
/**
|
||||||
* The workspace the document was moved from.
|
* The workspace the document was moved from.
|
||||||
*/
|
*/
|
||||||
workspace: {
|
previousWorkspace: {
|
||||||
/**
|
/**
|
||||||
* The ID of the workspace.
|
* The ID of the workspace.
|
||||||
*/
|
*/
|
||||||
@ -97,15 +172,10 @@ export interface AuditEventDetails {
|
|||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
/**
|
|
||||||
* The current workspace.
|
|
||||||
*/
|
|
||||||
current: {
|
|
||||||
/**
|
/**
|
||||||
* The workspace the document was moved to.
|
* The workspace the document was moved to.
|
||||||
*/
|
*/
|
||||||
workspace: {
|
newWorkspace: {
|
||||||
/**
|
/**
|
||||||
* The ID of the workspace.
|
* The ID of the workspace.
|
||||||
*/
|
*/
|
||||||
@ -116,10 +186,6 @@ export interface AuditEventDetails {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
/**
|
|
||||||
* A document was moved to the trash.
|
|
||||||
*/
|
|
||||||
removeDocument: {
|
removeDocument: {
|
||||||
/**
|
/**
|
||||||
* The ID of the document.
|
* The ID of the document.
|
||||||
@ -130,9 +196,6 @@ export interface AuditEventDetails {
|
|||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* A document was permanently deleted.
|
|
||||||
*/
|
|
||||||
deleteDocument: {
|
deleteDocument: {
|
||||||
/**
|
/**
|
||||||
* The ID of the document.
|
* The ID of the document.
|
||||||
@ -143,14 +206,7 @@ export interface AuditEventDetails {
|
|||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* A document was restored from the trash.
|
|
||||||
*/
|
|
||||||
restoreDocumentFromTrash: {
|
restoreDocumentFromTrash: {
|
||||||
/**
|
|
||||||
* The restored document.
|
|
||||||
*/
|
|
||||||
document: {
|
|
||||||
/**
|
/**
|
||||||
* The ID of the document.
|
* The ID of the document.
|
||||||
*/
|
*/
|
||||||
@ -159,9 +215,76 @@ export interface AuditEventDetails {
|
|||||||
* The name of the document.
|
* The name of the document.
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* The workspace of the restored document.
|
* The workspace of the document.
|
||||||
|
*/
|
||||||
|
workspace: {
|
||||||
|
/**
|
||||||
|
* The ID of the workspace.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The name of the workspace.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
changeDocumentAccess: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The access level of the document.
|
||||||
|
*/
|
||||||
|
access: {
|
||||||
|
/**
|
||||||
|
* The max inherited role.
|
||||||
|
*/
|
||||||
|
maxInheritedRole?: BasicRole | null;
|
||||||
|
/**
|
||||||
|
* The access level by user ID.
|
||||||
|
*/
|
||||||
|
users?: Record<string, NonGuestRole | null>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
openDocument: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The name of the document.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* The URL ID of the document.
|
||||||
|
*/
|
||||||
|
urlId: string;
|
||||||
|
/**
|
||||||
|
* The ID of the fork, if the document is a fork.
|
||||||
|
*/
|
||||||
|
forkId?: string;
|
||||||
|
/**
|
||||||
|
* The ID of the snapshot, if the document is a snapshot.
|
||||||
|
*/
|
||||||
|
snapshotId?: string;
|
||||||
|
};
|
||||||
|
duplicateDocument: {
|
||||||
|
/**
|
||||||
|
* The document that was duplicated.
|
||||||
|
*/
|
||||||
|
original: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The name of the document.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* The workspace of the document.
|
||||||
*/
|
*/
|
||||||
workspace: {
|
workspace: {
|
||||||
/**
|
/**
|
||||||
@ -175,8 +298,107 @@ export interface AuditEventDetails {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* A SQL query was run against a document.
|
* The newly-duplicated document.
|
||||||
*/
|
*/
|
||||||
|
duplicate: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The name of the document.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* If the document was duplicated without any data from the original document.
|
||||||
|
*/
|
||||||
|
asTemplate: boolean;
|
||||||
|
};
|
||||||
|
forkDocument: {
|
||||||
|
/**
|
||||||
|
* The document that was forked.
|
||||||
|
*/
|
||||||
|
original: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The name of the document.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The newly-forked document.
|
||||||
|
*/
|
||||||
|
fork: {
|
||||||
|
/**
|
||||||
|
* The ID of the fork.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The ID of the fork with the trunk ID.
|
||||||
|
*/
|
||||||
|
documentId: string;
|
||||||
|
/**
|
||||||
|
* The ID of the fork with the trunk URL ID.
|
||||||
|
*/
|
||||||
|
urlId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
replaceDocument: {
|
||||||
|
/**
|
||||||
|
* The document that was replaced.
|
||||||
|
*/
|
||||||
|
previous: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The newly-replaced document.
|
||||||
|
*/
|
||||||
|
current: {
|
||||||
|
/**
|
||||||
|
* The ID of the document.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The ID of the snapshot, if the document was replaced with one.
|
||||||
|
*/
|
||||||
|
snapshotId?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
reloadDocument: {},
|
||||||
|
truncateDocumentHistory: {
|
||||||
|
/**
|
||||||
|
* The number of history items kept.
|
||||||
|
*/
|
||||||
|
keep: number;
|
||||||
|
},
|
||||||
|
deliverWebhookEvents: {
|
||||||
|
/**
|
||||||
|
* The ID of the webhook.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The host the webhook events were delivered to.
|
||||||
|
*/
|
||||||
|
host: string;
|
||||||
|
/**
|
||||||
|
* The number of webhook events delivered.
|
||||||
|
*/
|
||||||
|
quantity: number;
|
||||||
|
},
|
||||||
|
clearWebhookQueue: {
|
||||||
|
/**
|
||||||
|
* The ID of the webhook.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
},
|
||||||
|
clearAllWebhookQueues: {},
|
||||||
runSQLQuery: {
|
runSQLQuery: {
|
||||||
/**
|
/**
|
||||||
* The SQL query.
|
* The SQL query.
|
||||||
@ -185,12 +407,169 @@ export interface AuditEventDetails {
|
|||||||
/**
|
/**
|
||||||
* The arguments used for query parameters, if any.
|
* The arguments used for query parameters, if any.
|
||||||
*/
|
*/
|
||||||
arguments?: (string | number)[];
|
arguments?: Array<string | number>;
|
||||||
/**
|
/**
|
||||||
* The duration in milliseconds until query execution should time out.
|
* The query execution timeout duration in milliseconds.
|
||||||
*/
|
*/
|
||||||
timeout?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
createWorkspace: {
|
||||||
|
/**
|
||||||
|
* The ID of the workspace.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The name of the workspace.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
renameWorkspace: {
|
||||||
|
/**
|
||||||
|
* The ID of the workspace.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The previous name of the workspace.
|
||||||
|
*/
|
||||||
|
previousName: string;
|
||||||
|
/**
|
||||||
|
* The current name of the workspace.
|
||||||
|
*/
|
||||||
|
currentName: string;
|
||||||
|
};
|
||||||
|
removeWorkspace: {
|
||||||
|
/**
|
||||||
|
* The ID of the workspace.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The name of the workspace.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
deleteWorkspace: {
|
||||||
|
/**
|
||||||
|
* The ID of the workspace.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The name of the workspace.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
restoreWorkspaceFromTrash: {
|
||||||
|
/**
|
||||||
|
* The ID of the workspace.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The name of the workspace.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
changeWorkspaceAccess: {
|
||||||
|
/**
|
||||||
|
* The ID of the workspace.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The access level of the workspace.
|
||||||
|
*/
|
||||||
|
access: {
|
||||||
|
/**
|
||||||
|
* The max inherited role.
|
||||||
|
*/
|
||||||
|
maxInheritedRole?: BasicRole | null;
|
||||||
|
/**
|
||||||
|
* The access level by user ID.
|
||||||
|
*/
|
||||||
|
users?: Record<string, NonGuestRole | null>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
createSite: {
|
||||||
|
/**
|
||||||
|
* The ID of the site.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The name of the site.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* The domain of the site.
|
||||||
|
*/
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
renameSite: {
|
||||||
|
/**
|
||||||
|
* The ID of the site.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The previous name and domain of the site.
|
||||||
|
*/
|
||||||
|
previous: {
|
||||||
|
/**
|
||||||
|
* The name of the site.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* The domain of the site.
|
||||||
|
*/
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The current name and domain of the site.
|
||||||
|
*/
|
||||||
|
current: {
|
||||||
|
/**
|
||||||
|
* The name of the site.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* The domain of the site.
|
||||||
|
*/
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
deleteSite: {
|
||||||
|
/**
|
||||||
|
* The ID of the site.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The name of the site.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
changeSiteAccess: {
|
||||||
|
/**
|
||||||
|
* The ID of the site.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The access level of the site.
|
||||||
|
*/
|
||||||
|
access: {
|
||||||
|
/**
|
||||||
|
* The access level by user ID.
|
||||||
|
*/
|
||||||
|
users?: Record<string, NonGuestRole | null>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
changeUserName: {
|
||||||
|
/**
|
||||||
|
* The previous name of the user.
|
||||||
|
*/
|
||||||
|
previousName: string;
|
||||||
|
/**
|
||||||
|
* The current name of the user.
|
||||||
|
*/
|
||||||
|
currentName: string;
|
||||||
|
};
|
||||||
|
createUserAPIKey: {};
|
||||||
|
deleteUserAPIKey: {};
|
||||||
|
deleteUser: {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuditEventContext {
|
export interface AuditEventContext {
|
||||||
@ -206,7 +585,7 @@ export interface AuditEventContext {
|
|||||||
|
|
||||||
export interface AuditEventSource {
|
export interface AuditEventSource {
|
||||||
/**
|
/**
|
||||||
* The domain of the org tied to the originating request.
|
* The domain of the site tied to the originating request.
|
||||||
*/
|
*/
|
||||||
org?: string;
|
org?: string;
|
||||||
/**
|
/**
|
||||||
|
@ -9,7 +9,9 @@ import {FullUser} from 'app/common/LoginSessionAPI';
|
|||||||
import {BasicRole} from 'app/common/roles';
|
import {BasicRole} from 'app/common/roles';
|
||||||
import {OrganizationProperties, PermissionDelta} from 'app/common/UserAPI';
|
import {OrganizationProperties, PermissionDelta} from 'app/common/UserAPI';
|
||||||
import {Document} from "app/gen-server/entity/Document";
|
import {Document} from "app/gen-server/entity/Document";
|
||||||
|
import {Organization} from 'app/gen-server/entity/Organization';
|
||||||
import {User} from 'app/gen-server/entity/User';
|
import {User} from 'app/gen-server/entity/User';
|
||||||
|
import {Workspace} from 'app/gen-server/entity/Workspace';
|
||||||
import {BillingOptions, HomeDBManager, Scope} from 'app/gen-server/lib/homedb/HomeDBManager';
|
import {BillingOptions, HomeDBManager, Scope} from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||||
import {PreviousAndCurrent, QueryResult} from 'app/gen-server/lib/homedb/Interfaces';
|
import {PreviousAndCurrent, QueryResult} from 'app/gen-server/lib/homedb/Interfaces';
|
||||||
import {getAuthorizedUserId, getUserId, getUserProfiles, RequestWithLogin} from 'app/server/lib/Authorizer';
|
import {getAuthorizedUserId, getUserId, getUserProfiles, RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||||
@ -77,7 +79,7 @@ export function addOrg(
|
|||||||
product?: string,
|
product?: string,
|
||||||
billing?: BillingOptions,
|
billing?: BillingOptions,
|
||||||
}
|
}
|
||||||
): Promise<number> {
|
): Promise<Organization> {
|
||||||
return dbManager.connection.transaction(async manager => {
|
return dbManager.connection.transaction(async manager => {
|
||||||
const user = await manager.findOne(User, {where: {id: userId}});
|
const user = await manager.findOne(User, {where: {id: userId}});
|
||||||
if (!user) { return handleDeletedUser(); }
|
if (!user) { return handleDeletedUser(); }
|
||||||
@ -167,8 +169,9 @@ export class ApiServer {
|
|||||||
// doesn't have access to that information yet, so punting on this.
|
// doesn't have access to that information yet, so punting on this.
|
||||||
// TODO: figure out who should be allowed to create organizations
|
// TODO: figure out who should be allowed to create organizations
|
||||||
const userId = getAuthorizedUserId(req);
|
const userId = getAuthorizedUserId(req);
|
||||||
const orgId = await addOrg(this._dbManager, userId, req.body);
|
const org = await addOrg(this._dbManager, userId, req.body);
|
||||||
return sendOkReply(req, res, orgId);
|
this._logCreateSiteEvents(req, org);
|
||||||
|
return sendOkReply(req, res, org.id);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// PATCH /api/orgs/:oid
|
// PATCH /api/orgs/:oid
|
||||||
@ -176,32 +179,30 @@ export class ApiServer {
|
|||||||
// Update the specified org.
|
// Update the specified org.
|
||||||
this._app.patch('/api/orgs/:oid', expressWrap(async (req, res) => {
|
this._app.patch('/api/orgs/:oid', expressWrap(async (req, res) => {
|
||||||
const org = getOrgKey(req);
|
const org = getOrgKey(req);
|
||||||
const query = await this._dbManager.updateOrg(getScope(req), org, req.body);
|
const {data, ...result} = await this._dbManager.updateOrg(getScope(req), org, req.body);
|
||||||
return sendReply(req, res, query);
|
if (data && (req.body.name || req.body.domain)) {
|
||||||
|
this._logRenameSiteEvents(req as RequestWithLogin, data);
|
||||||
|
}
|
||||||
|
return sendReply(req, res, result);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// DELETE /api/orgs/:oid
|
// DELETE /api/orgs/:oid
|
||||||
// Delete the specified org and all included workspaces and docs.
|
// Delete the specified org and all included workspaces and docs.
|
||||||
this._app.delete('/api/orgs/:oid', expressWrap(async (req, res) => {
|
this._app.delete('/api/orgs/:oid', expressWrap(async (req, res) => {
|
||||||
const org = getOrgKey(req);
|
const org = getOrgKey(req);
|
||||||
const query = await this._dbManager.deleteOrg(getScope(req), org);
|
const {data, ...result} = await this._dbManager.deleteOrg(getScope(req), org);
|
||||||
return sendReply(req, res, query);
|
if (data) { this._logDeleteSiteEvents(req, data); }
|
||||||
|
return sendReply(req, res, {...result, data: data?.id});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// POST /api/orgs/:oid/workspaces
|
// POST /api/orgs/:oid/workspaces
|
||||||
// Body params: name
|
// Body params: name
|
||||||
// Create a new workspace owned by the specific organization.
|
// Create a new workspace owned by the specific organization.
|
||||||
this._app.post('/api/orgs/:oid/workspaces', expressWrap(async (req, res) => {
|
this._app.post('/api/orgs/:oid/workspaces', expressWrap(async (req, res) => {
|
||||||
const mreq = req as RequestWithLogin;
|
|
||||||
const org = getOrgKey(req);
|
const org = getOrgKey(req);
|
||||||
const query = await this._dbManager.addWorkspace(getScope(req), org, req.body);
|
const {data, ...result} = await this._dbManager.addWorkspace(getScope(req), org, req.body);
|
||||||
this._gristServer.getTelemetry().logEvent(mreq, 'createdWorkspace', {
|
if (data) { this._logCreateWorkspaceEvents(req, data); }
|
||||||
full: {
|
return sendReply(req, res, {...result, data: data?.id});
|
||||||
workspaceId: query.data,
|
|
||||||
userId: mreq.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return sendReply(req, res, query);
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// PATCH /api/workspaces/:wid
|
// PATCH /api/workspaces/:wid
|
||||||
@ -209,23 +210,18 @@ export class ApiServer {
|
|||||||
// Update the specified workspace.
|
// Update the specified workspace.
|
||||||
this._app.patch('/api/workspaces/:wid', expressWrap(async (req, res) => {
|
this._app.patch('/api/workspaces/:wid', expressWrap(async (req, res) => {
|
||||||
const wsId = integerParam(req.params.wid, 'wid');
|
const wsId = integerParam(req.params.wid, 'wid');
|
||||||
const query = await this._dbManager.updateWorkspace(getScope(req), wsId, req.body);
|
const {data, ...result} = await this._dbManager.updateWorkspace(getScope(req), wsId, req.body);
|
||||||
return sendReply(req, res, query);
|
if (data && 'name' in req.body) { this._logRenameWorkspaceEvents(req, data); }
|
||||||
|
return sendReply(req, res, {...result, data: data?.current.id});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// DELETE /api/workspaces/:wid
|
// DELETE /api/workspaces/:wid
|
||||||
// Delete the specified workspace and all included docs.
|
// Delete the specified workspace and all included docs.
|
||||||
this._app.delete('/api/workspaces/:wid', expressWrap(async (req, res) => {
|
this._app.delete('/api/workspaces/:wid', expressWrap(async (req, res) => {
|
||||||
const mreq = req as RequestWithLogin;
|
|
||||||
const wsId = integerParam(req.params.wid, 'wid');
|
const wsId = integerParam(req.params.wid, 'wid');
|
||||||
const query = await this._dbManager.deleteWorkspace(getScope(req), wsId);
|
const {data, ...result} = await this._dbManager.deleteWorkspace(getScope(req), wsId);
|
||||||
this._gristServer.getTelemetry().logEvent(mreq, 'deletedWorkspace', {
|
if (data) { this._logDeleteWorkspaceEvents(req, data); }
|
||||||
full: {
|
return sendReply(req, res, {...result, data: data?.id});
|
||||||
workspaceId: wsId,
|
|
||||||
userId: mreq.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return sendReply(req, res, query);
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// POST /api/workspaces/:wid/remove
|
// POST /api/workspaces/:wid/remove
|
||||||
@ -234,17 +230,12 @@ export class ApiServer {
|
|||||||
this._app.post('/api/workspaces/:wid/remove', expressWrap(async (req, res) => {
|
this._app.post('/api/workspaces/:wid/remove', expressWrap(async (req, res) => {
|
||||||
const wsId = integerParam(req.params.wid, 'wid');
|
const wsId = integerParam(req.params.wid, 'wid');
|
||||||
if (isParameterOn(req.query.permanent)) {
|
if (isParameterOn(req.query.permanent)) {
|
||||||
const mreq = req as RequestWithLogin;
|
const {data, ...result} = await this._dbManager.deleteWorkspace(getScope(req), wsId);
|
||||||
const query = await this._dbManager.deleteWorkspace(getScope(req), wsId);
|
if (data) { this._logDeleteWorkspaceEvents(req, data); }
|
||||||
this._gristServer.getTelemetry().logEvent(mreq, 'deletedWorkspace', {
|
return sendReply(req, res, {...result, data: data?.id});
|
||||||
full: {
|
|
||||||
workspaceId: query.data,
|
|
||||||
userId: mreq.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return sendReply(req, res, query);
|
|
||||||
} else {
|
} else {
|
||||||
await this._dbManager.softDeleteWorkspace(getScope(req), wsId);
|
const {data} = await this._dbManager.softDeleteWorkspace(getScope(req), wsId);
|
||||||
|
if (data) { this._logRemoveWorkspaceEvents(req, data); }
|
||||||
return sendOkReply(req, res);
|
return sendOkReply(req, res);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -254,7 +245,8 @@ export class ApiServer {
|
|||||||
// still available.
|
// still available.
|
||||||
this._app.post('/api/workspaces/:wid/unremove', expressWrap(async (req, res) => {
|
this._app.post('/api/workspaces/:wid/unremove', expressWrap(async (req, res) => {
|
||||||
const wsId = integerParam(req.params.wid, 'wid');
|
const wsId = integerParam(req.params.wid, 'wid');
|
||||||
await this._dbManager.undeleteWorkspace(getScope(req), wsId);
|
const {data} = await this._dbManager.undeleteWorkspace(getScope(req), wsId);
|
||||||
|
if (data) { this._logRestoreWorkspaceEvents(req, data); }
|
||||||
return sendOkReply(req, res);
|
return sendOkReply(req, res);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -262,9 +254,9 @@ export class ApiServer {
|
|||||||
// Create a new doc owned by the specific workspace.
|
// Create a new doc owned by the specific workspace.
|
||||||
this._app.post('/api/workspaces/:wid/docs', expressWrap(async (req, res) => {
|
this._app.post('/api/workspaces/:wid/docs', expressWrap(async (req, res) => {
|
||||||
const wsId = integerParam(req.params.wid, 'wid');
|
const wsId = integerParam(req.params.wid, 'wid');
|
||||||
const result = await this._dbManager.addDocument(getScope(req), wsId, req.body);
|
const {data, ...result} = await this._dbManager.addDocument(getScope(req), wsId, req.body);
|
||||||
if (result.status === 200) { this._logCreateDocumentEvents(req, result.data!); }
|
if (data) { this._logCreateDocumentEvents(req, data); }
|
||||||
return sendReply(req, res, {...result, data: result.data?.id});
|
return sendReply(req, res, {...result, data: data?.id});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// GET /api/templates/
|
// GET /api/templates/
|
||||||
@ -301,16 +293,17 @@ export class ApiServer {
|
|||||||
// PATCH /api/docs/:did
|
// PATCH /api/docs/:did
|
||||||
// Update the specified doc.
|
// Update the specified doc.
|
||||||
this._app.patch('/api/docs/:did', expressWrap(async (req, res) => {
|
this._app.patch('/api/docs/:did', expressWrap(async (req, res) => {
|
||||||
const query = await this._dbManager.updateDocument(getDocScope(req), req.body);
|
const {data, ...result} = await this._dbManager.updateDocument(getDocScope(req), req.body);
|
||||||
return sendReply(req, res, query);
|
if (data && 'name' in req.body) { this._logRenameDocumentEvents(req, data); }
|
||||||
|
return sendReply(req, res, {...result, data: data?.current.id});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// POST /api/docs/:did/unremove
|
// POST /api/docs/:did/unremove
|
||||||
// Recover the specified doc if it was previously soft-deleted and is
|
// Recover the specified doc if it was previously soft-deleted and is
|
||||||
// still available.
|
// still available.
|
||||||
this._app.post('/api/docs/:did/unremove', expressWrap(async (req, res) => {
|
this._app.post('/api/docs/:did/unremove', expressWrap(async (req, res) => {
|
||||||
const {status, data} = await this._dbManager.undeleteDocument(getDocScope(req));
|
const {data} = await this._dbManager.undeleteDocument(getDocScope(req));
|
||||||
if (status === 200) { this._logRestoreDocumentEvents(req, data!); }
|
if (data) { this._logRestoreDocumentEvents(req, data); }
|
||||||
return sendOkReply(req, res);
|
return sendOkReply(req, res);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -319,8 +312,9 @@ export class ApiServer {
|
|||||||
this._app.patch('/api/orgs/:oid/access', expressWrap(async (req, res) => {
|
this._app.patch('/api/orgs/:oid/access', expressWrap(async (req, res) => {
|
||||||
const org = getOrgKey(req);
|
const org = getOrgKey(req);
|
||||||
const delta = req.body.delta;
|
const delta = req.body.delta;
|
||||||
const query = await this._dbManager.updateOrgPermissions(getScope(req), org, delta);
|
const {data, ...result} = await this._dbManager.updateOrgPermissions(getScope(req), org, delta);
|
||||||
return sendReply(req, res, query);
|
if (data) { this._logChangeSiteAccessEvents(req as RequestWithLogin, data); }
|
||||||
|
return sendReply(req, res, result);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// PATCH /api/workspaces/:wid/access
|
// PATCH /api/workspaces/:wid/access
|
||||||
@ -328,8 +322,9 @@ export class ApiServer {
|
|||||||
this._app.patch('/api/workspaces/:wid/access', expressWrap(async (req, res) => {
|
this._app.patch('/api/workspaces/:wid/access', expressWrap(async (req, res) => {
|
||||||
const workspaceId = integerParam(req.params.wid, 'wid');
|
const workspaceId = integerParam(req.params.wid, 'wid');
|
||||||
const delta = req.body.delta;
|
const delta = req.body.delta;
|
||||||
const query = await this._dbManager.updateWorkspacePermissions(getScope(req), workspaceId, delta);
|
const {data, ...result} = await this._dbManager.updateWorkspacePermissions(getScope(req), workspaceId, delta);
|
||||||
return sendReply(req, res, query);
|
if (data) { this._logChangeWorkspaceAccessEvents(req as RequestWithLogin, data); }
|
||||||
|
return sendReply(req, res, result);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// GET /api/docs/:did
|
// GET /api/docs/:did
|
||||||
@ -343,28 +338,30 @@ export class ApiServer {
|
|||||||
// Update the specified doc acl rules.
|
// Update the specified doc acl rules.
|
||||||
this._app.patch('/api/docs/:did/access', expressWrap(async (req, res) => {
|
this._app.patch('/api/docs/:did/access', expressWrap(async (req, res) => {
|
||||||
const delta = req.body.delta;
|
const delta = req.body.delta;
|
||||||
const query = await this._dbManager.updateDocPermissions(getDocScope(req), delta);
|
const {data, ...result} = await this._dbManager.updateDocPermissions(getDocScope(req), delta);
|
||||||
this._logInvitedDocUserTelemetryEvents(req as RequestWithLogin, delta);
|
if (data) { this._logChangeDocumentAccessEvents(req as RequestWithLogin, data); }
|
||||||
return sendReply(req, res, query);
|
return sendReply(req, res, result);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// PATCH /api/docs/:did/move
|
// PATCH /api/docs/:did/move
|
||||||
// Move the doc to the workspace specified in the body.
|
// Move the doc to the workspace specified in the body.
|
||||||
this._app.patch('/api/docs/:did/move', expressWrap(async (req, res) => {
|
this._app.patch('/api/docs/:did/move', expressWrap(async (req, res) => {
|
||||||
const workspaceId = integerParam(req.body.workspace, 'workspace');
|
const workspaceId = integerParam(req.body.workspace, 'workspace');
|
||||||
const result = await this._dbManager.moveDoc(getDocScope(req), workspaceId);
|
const {data, ...result} = await this._dbManager.moveDoc(getDocScope(req), workspaceId);
|
||||||
if (result.status === 200) { this._logMoveDocumentEvents(req, result.data!); }
|
if (data) { this._logMoveDocumentEvents(req, data); }
|
||||||
return sendReply(req, res, {...result, data: result.data?.current.id});
|
return sendReply(req, res, {...result, data: data?.current.id});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this._app.patch('/api/docs/:did/pin', expressWrap(async (req, res) => {
|
this._app.patch('/api/docs/:did/pin', expressWrap(async (req, res) => {
|
||||||
const query = await this._dbManager.pinDoc(getDocScope(req), true);
|
const {data, ...result} = await this._dbManager.pinDoc(getDocScope(req), true);
|
||||||
return sendReply(req, res, query);
|
if (data) { this._logPinDocumentEvents(req, data); }
|
||||||
|
return sendReply(req, res, result);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this._app.patch('/api/docs/:did/unpin', expressWrap(async (req, res) => {
|
this._app.patch('/api/docs/:did/unpin', expressWrap(async (req, res) => {
|
||||||
const query = await this._dbManager.pinDoc(getDocScope(req), false);
|
const {data, ...result} = await this._dbManager.pinDoc(getDocScope(req), false);
|
||||||
return sendReply(req, res, query);
|
if (data) { this._logUnpinDocumentEvents(req, data); }
|
||||||
|
return sendReply(req, res, result);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// GET /api/orgs/:oid/access
|
// GET /api/orgs/:oid/access
|
||||||
@ -408,7 +405,8 @@ export class ApiServer {
|
|||||||
throw new ApiError('Name expected in the body', 400);
|
throw new ApiError('Name expected in the body', 400);
|
||||||
}
|
}
|
||||||
const name = req.body.name;
|
const name = req.body.name;
|
||||||
await this._dbManager.updateUser(userId, { name });
|
const {previous, current} = await this._dbManager.updateUser(userId, { name });
|
||||||
|
this._logChangeUserNameEvents(req, {previous, current});
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -489,6 +487,7 @@ export class ApiServer {
|
|||||||
if (!user) { return handleDeletedUser(); }
|
if (!user) { return handleDeletedUser(); }
|
||||||
if (!user.apiKey || force) {
|
if (!user.apiKey || force) {
|
||||||
user = await updateApiKeyWithRetry(manager, user);
|
user = await updateApiKeyWithRetry(manager, user);
|
||||||
|
this._logCreateUserAPIKeyEvents(req);
|
||||||
res.status(200).send(user.apiKey);
|
res.status(200).send(user.apiKey);
|
||||||
} else {
|
} else {
|
||||||
res.status(400).send({error: "An apikey is already set, use `{force: true}` to override it."});
|
res.status(400).send({error: "An apikey is already set, use `{force: true}` to override it."});
|
||||||
@ -504,6 +503,7 @@ export class ApiServer {
|
|||||||
if (!user) { return handleDeletedUser(); }
|
if (!user) { return handleDeletedUser(); }
|
||||||
user.apiKey = null;
|
user.apiKey = null;
|
||||||
await manager.save(User, user);
|
await manager.save(User, user);
|
||||||
|
this._logDeleteUserAPIKeyEvents(req);
|
||||||
});
|
});
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
}));
|
}));
|
||||||
@ -656,16 +656,31 @@ export class ApiServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _logRenameDocumentEvents(
|
||||||
|
req: Request,
|
||||||
|
{previous, current}: PreviousAndCurrent<Document>
|
||||||
|
) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'renameDocument',
|
||||||
|
details: {
|
||||||
|
id: current.id,
|
||||||
|
previousName: previous.name,
|
||||||
|
currentName: current.name,
|
||||||
|
},
|
||||||
|
context: {workspaceId: current.workspace.id},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _logRestoreDocumentEvents(req: Request, document: Document) {
|
private _logRestoreDocumentEvents(req: Request, document: Document) {
|
||||||
const {workspace} = document;
|
const {id, name, workspace} = document;
|
||||||
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
event: {
|
event: {
|
||||||
name: 'restoreDocumentFromTrash',
|
name: 'restoreDocumentFromTrash',
|
||||||
details: {
|
details: {
|
||||||
document: {
|
id,
|
||||||
id: document.id,
|
name,
|
||||||
name: document.name,
|
|
||||||
},
|
|
||||||
workspace: {
|
workspace: {
|
||||||
id: workspace.id,
|
id: workspace.id,
|
||||||
name: workspace.name,
|
name: workspace.name,
|
||||||
@ -675,6 +690,27 @@ export class ApiServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _logChangeDocumentAccessEvents(
|
||||||
|
req: RequestWithLogin,
|
||||||
|
{document, maxInheritedRole, users}: PermissionDelta & {document: Document}
|
||||||
|
) {
|
||||||
|
const {id, workspace: {id: workspaceId}} = document;
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'changeDocumentAccess',
|
||||||
|
details: {
|
||||||
|
id,
|
||||||
|
access: {
|
||||||
|
maxInheritedRole,
|
||||||
|
users,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {workspaceId},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this._logInvitedDocUserTelemetryEvents(req, {maxInheritedRole, users});
|
||||||
|
}
|
||||||
|
|
||||||
private _logInvitedDocUserTelemetryEvents(mreq: RequestWithLogin, delta: PermissionDelta) {
|
private _logInvitedDocUserTelemetryEvents(mreq: RequestWithLogin, delta: PermissionDelta) {
|
||||||
if (!delta.users) { return; }
|
if (!delta.users) { return; }
|
||||||
|
|
||||||
@ -722,25 +758,207 @@ export class ApiServer {
|
|||||||
name: 'moveDocument',
|
name: 'moveDocument',
|
||||||
details: {
|
details: {
|
||||||
id: current.id,
|
id: current.id,
|
||||||
previous: {
|
previousWorkspace: {
|
||||||
workspace: {
|
|
||||||
id: previous.workspace.id,
|
id: previous.workspace.id,
|
||||||
name: previous.workspace.name,
|
name: previous.workspace.name,
|
||||||
},
|
},
|
||||||
},
|
newWorkspace: {
|
||||||
current: {
|
|
||||||
workspace: {
|
|
||||||
id: current.workspace.id,
|
id: current.workspace.id,
|
||||||
name: current.workspace.name,
|
name: current.workspace.name,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
context: {
|
context: {
|
||||||
workspaceId: previous.workspace.id,
|
workspaceId: previous.workspace.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _logPinDocumentEvents(req: Request, document: Document) {
|
||||||
|
const {id, name, workspace: {id: workspaceId}} = document;
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'pinDocument',
|
||||||
|
details: {id, name},
|
||||||
|
context: {workspaceId},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logUnpinDocumentEvents(req: Request, document: Document) {
|
||||||
|
const {id, name, workspace: {id: workspaceId}} = document;
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'unpinDocument',
|
||||||
|
details: {id, name},
|
||||||
|
context: {workspaceId},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logCreateWorkspaceEvents(req: Request, {id, name}: Workspace) {
|
||||||
|
const mreq = req as RequestWithLogin;
|
||||||
|
this._gristServer.getAuditLogger().logEvent(mreq, {
|
||||||
|
event: {
|
||||||
|
name: 'createWorkspace',
|
||||||
|
details: {id, name},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this._gristServer.getTelemetry().logEvent(mreq, 'createdWorkspace', {
|
||||||
|
full: {
|
||||||
|
workspaceId: id,
|
||||||
|
userId: mreq.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logRenameWorkspaceEvents(
|
||||||
|
req: Request,
|
||||||
|
{previous, current}: PreviousAndCurrent<Workspace>
|
||||||
|
) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'renameWorkspace',
|
||||||
|
details: {
|
||||||
|
id: current.id,
|
||||||
|
previousName: previous.name,
|
||||||
|
currentName: current.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logRemoveWorkspaceEvents(req: Request, {id, name}: Workspace) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'removeWorkspace',
|
||||||
|
details: {id, name},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logDeleteWorkspaceEvents(req: Request, {id, name}: Workspace) {
|
||||||
|
const mreq = req as RequestWithLogin;
|
||||||
|
this._gristServer.getAuditLogger().logEvent(mreq, {
|
||||||
|
event: {
|
||||||
|
name: 'deleteWorkspace',
|
||||||
|
details: {id, name},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this._gristServer.getTelemetry().logEvent(mreq, 'deletedWorkspace', {
|
||||||
|
full: {
|
||||||
|
workspaceId: id,
|
||||||
|
userId: mreq.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logRestoreWorkspaceEvents(req: Request, {id, name}: Workspace) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'restoreWorkspaceFromTrash',
|
||||||
|
details: {id, name},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logChangeWorkspaceAccessEvents(
|
||||||
|
req: RequestWithLogin,
|
||||||
|
{workspace: {id}, maxInheritedRole, users}: PermissionDelta & {workspace: Workspace}
|
||||||
|
) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'changeWorkspaceAccess',
|
||||||
|
details: {
|
||||||
|
id,
|
||||||
|
access: {
|
||||||
|
maxInheritedRole,
|
||||||
|
users,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logCreateSiteEvents(req: Request, {id, name, domain}: Organization) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'createSite',
|
||||||
|
details: {id, name, domain},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logRenameSiteEvents(
|
||||||
|
req: Request,
|
||||||
|
{previous, current}: PreviousAndCurrent<Organization>
|
||||||
|
) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'renameSite',
|
||||||
|
details: {
|
||||||
|
id: current.id,
|
||||||
|
previous: {
|
||||||
|
name: previous.name,
|
||||||
|
domain: previous.domain,
|
||||||
|
},
|
||||||
|
current: {
|
||||||
|
name: current.name,
|
||||||
|
domain: current.domain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logDeleteSiteEvents(req: Request, {id, name}: Organization) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'deleteSite',
|
||||||
|
details: {id, name},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logChangeSiteAccessEvents(
|
||||||
|
req: RequestWithLogin,
|
||||||
|
{organization: {id}, users}: PermissionDelta & {organization: Organization}
|
||||||
|
) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'changeSiteAccess',
|
||||||
|
details: {id, access: {users}},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logChangeUserNameEvents(
|
||||||
|
req: Request,
|
||||||
|
{previous: {name: previousName}, current: {name: currentName}}: PreviousAndCurrent<User>
|
||||||
|
) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'changeUserName',
|
||||||
|
details: {previousName, currentName},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logCreateUserAPIKeyEvents(req: Request) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'createUserAPIKey',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logDeleteUserAPIKeyEvents(req: Request) {
|
||||||
|
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||||
|
event: {
|
||||||
|
name: 'deleteUserAPIKey',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -459,11 +459,15 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
return await this._usersManager.ensureExternalUser(profile);
|
return await this._usersManager.ensureExternalUser(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateUser(userId: number, props: UserProfileChange) {
|
public async updateUser(
|
||||||
const { user, isWelcomed } = await this._usersManager.updateUser(userId, props);
|
userId: number,
|
||||||
if (user && isWelcomed) {
|
props: UserProfileChange
|
||||||
this.emit('firstLogin', this.makeFullUser(user));
|
): Promise<PreviousAndCurrent<User>> {
|
||||||
|
const {previous, current, isWelcomed} = await this._usersManager.updateUser(userId, props);
|
||||||
|
if (current && isWelcomed) {
|
||||||
|
this.emit('firstLogin', this.makeFullUser(current));
|
||||||
}
|
}
|
||||||
|
return {previous, current};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateUserOptions(userId: number, props: Partial<UserOptions>) {
|
public async updateUserOptions(userId: number, props: Partial<UserOptions>) {
|
||||||
@ -1058,7 +1062,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Adds an org with the given name. Returns a query result with the id of the added org.
|
* Adds an org with the given name. Returns a query result with the added org.
|
||||||
*
|
*
|
||||||
* @param user: user doing the adding
|
* @param user: user doing the adding
|
||||||
* @param name: desired org name
|
* @param name: desired org name
|
||||||
@ -1073,12 +1077,17 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
* meaningful for team sites currently.
|
* meaningful for team sites currently.
|
||||||
* @param billing: if set, controls the billing account settings for the org.
|
* @param billing: if set, controls the billing account settings for the org.
|
||||||
*/
|
*/
|
||||||
public async addOrg(user: User, props: Partial<OrganizationProperties>,
|
public async addOrg(
|
||||||
options: { setUserAsOwner: boolean,
|
user: User,
|
||||||
|
props: Partial<OrganizationProperties>,
|
||||||
|
options: {
|
||||||
|
setUserAsOwner: boolean,
|
||||||
useNewPlan: boolean,
|
useNewPlan: boolean,
|
||||||
product?: string, // Default to PERSONAL_FREE_PLAN or TEAM_FREE_PLAN env variable.
|
product?: string, // Default to PERSONAL_FREE_PLAN or TEAM_FREE_PLAN env variable.
|
||||||
billing?: BillingOptions},
|
billing?: BillingOptions
|
||||||
transaction?: EntityManager): Promise<QueryResult<number>> {
|
},
|
||||||
|
transaction?: EntityManager
|
||||||
|
): Promise<QueryResult<Organization>> {
|
||||||
const notifications: Array<() => void> = [];
|
const notifications: Array<() => void> = [];
|
||||||
const name = props.name;
|
const name = props.name;
|
||||||
const domain = props.domain;
|
const domain = props.domain;
|
||||||
@ -1219,10 +1228,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
// Emit a notification.
|
// Emit a notification.
|
||||||
notifications.push(this._teamCreatorNotification(user.id));
|
notifications.push(this._teamCreatorNotification(user.id));
|
||||||
}
|
}
|
||||||
return {
|
return {status: 200, data: savedOrg};
|
||||||
status: 200,
|
|
||||||
data: savedOrg.id
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
for (const notification of notifications) { notification(); }
|
for (const notification of notifications) { notification(); }
|
||||||
return orgResult;
|
return orgResult;
|
||||||
@ -1230,8 +1236,8 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
|
|
||||||
// If setting anything more than prefs:
|
// If setting anything more than prefs:
|
||||||
// Checks that the user has UPDATE permissions to the given org. If not, throws an
|
// Checks that the user has UPDATE permissions to the given org. If not, throws an
|
||||||
// error. Otherwise updates the given org with the given name. Returns an empty
|
// error. Otherwise updates the given org with the given name. Returns a query
|
||||||
// query result with status 200 on success.
|
// result with status 200 on success.
|
||||||
// For setting userPrefs or userOrgPrefs:
|
// For setting userPrefs or userOrgPrefs:
|
||||||
// These are user-specific setting, so are allowed with VIEW access (that includes
|
// These are user-specific setting, so are allowed with VIEW access (that includes
|
||||||
// guests). Prefs are replaced in their entirety, not merged.
|
// guests). Prefs are replaced in their entirety, not merged.
|
||||||
@ -1242,7 +1248,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
orgKey: string|number,
|
orgKey: string|number,
|
||||||
props: Partial<OrganizationProperties>,
|
props: Partial<OrganizationProperties>,
|
||||||
transaction?: EntityManager,
|
transaction?: EntityManager,
|
||||||
): Promise<QueryResult<number>> {
|
): Promise<QueryResult<PreviousAndCurrent<Organization>>> {
|
||||||
|
|
||||||
// Check the scope of the modifications.
|
// Check the scope of the modifications.
|
||||||
let markPermissions: number = Permissions.VIEW;
|
let markPermissions: number = Permissions.VIEW;
|
||||||
@ -1272,11 +1278,12 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
const queryResult = await verifyEntity(orgQuery);
|
const queryResult = await verifyEntity(orgQuery);
|
||||||
if (queryResult.status !== 200) {
|
if (queryResult.status !== 200) {
|
||||||
// If the query for the workspace failed, return the failure result.
|
// If the query for the org failed, return the failure result.
|
||||||
return queryResult;
|
return queryResult;
|
||||||
}
|
}
|
||||||
// Update the fields and save.
|
// Update the fields and save.
|
||||||
const org: Organization = queryResult.data;
|
const org: Organization = queryResult.data;
|
||||||
|
const previous = structuredClone(org);
|
||||||
org.checkProperties(props);
|
org.checkProperties(props);
|
||||||
if (modifyOrg) {
|
if (modifyOrg) {
|
||||||
if (props.domain) {
|
if (props.domain) {
|
||||||
@ -1312,15 +1319,18 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {status: 200};
|
return {status: 200, data: {previous, current: org}};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the user has REMOVE permissions to the given org. If not, throws an
|
// Checks that the user has REMOVE permissions to the given org. If not, throws an
|
||||||
// error. Otherwise deletes the given org. Returns an empty query result with
|
// error. Otherwise deletes the given org. Returns a query result with status 200
|
||||||
// status 200 on success.
|
// on success.
|
||||||
public async deleteOrg(scope: Scope, orgKey: string|number,
|
public async deleteOrg(
|
||||||
transaction?: EntityManager): Promise<QueryResult<number>> {
|
scope: Scope,
|
||||||
|
orgKey: string|number,
|
||||||
|
transaction?: EntityManager
|
||||||
|
): Promise<QueryResult<Organization>> {
|
||||||
return await this._runInTransaction(transaction, async manager => {
|
return await this._runInTransaction(transaction, async manager => {
|
||||||
const orgQuery = this.org(scope, orgKey, {
|
const orgQuery = this.org(scope, orgKey, {
|
||||||
manager,
|
manager,
|
||||||
@ -1344,6 +1354,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
return queryResult;
|
return queryResult;
|
||||||
}
|
}
|
||||||
const org: Organization = queryResult.data;
|
const org: Organization = queryResult.data;
|
||||||
|
const deletedOrg = structuredClone(org);
|
||||||
// Delete the org, org ACLs/groups, workspaces, workspace ACLs/groups, workspace docs
|
// Delete the org, org ACLs/groups, workspaces, workspace ACLs/groups, workspace docs
|
||||||
// and doc ACLs/groups.
|
// and doc ACLs/groups.
|
||||||
const orgGroups = org.aclRules.map(orgAcl => orgAcl.group);
|
const orgGroups = org.aclRules.map(orgAcl => orgAcl.group);
|
||||||
@ -1363,15 +1374,18 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
if (billingAccount && billingAccount.orgs.length === 0) {
|
if (billingAccount && billingAccount.orgs.length === 0) {
|
||||||
await manager.remove([billingAccount]);
|
await manager.remove([billingAccount]);
|
||||||
}
|
}
|
||||||
return {status: 200};
|
return {status: 200, data: deletedOrg};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the user has ADD permissions to the given org. If not, throws an error.
|
// Checks that the user has ADD permissions to the given org. If not, throws an error.
|
||||||
// Otherwise adds a workspace with the given name. Returns a query result with the id
|
// Otherwise adds a workspace with the given name. Returns a query result with the
|
||||||
// of the added workspace.
|
// added workspace.
|
||||||
public async addWorkspace(scope: Scope, orgKey: string|number,
|
public async addWorkspace(
|
||||||
props: Partial<WorkspaceProperties>): Promise<QueryResult<number>> {
|
scope: Scope,
|
||||||
|
orgKey: string|number,
|
||||||
|
props: Partial<WorkspaceProperties>
|
||||||
|
): Promise<QueryResult<Workspace>> {
|
||||||
const name = props.name;
|
const name = props.name;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return {
|
return {
|
||||||
@ -1414,18 +1428,18 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const workspace = await this._doAddWorkspace({org, props, ownerId: scope.userId}, manager);
|
const workspace = await this._doAddWorkspace({org, props, ownerId: scope.userId}, manager);
|
||||||
return {
|
return {status: 200, data: workspace};
|
||||||
status: 200,
|
|
||||||
data: workspace.id
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the user has UPDATE permissions to the given workspace. If not, throws an
|
// Checks that the user has UPDATE permissions to the given workspace. If not, throws an
|
||||||
// error. Otherwise updates the given workspace with the given name. Returns an empty
|
// error. Otherwise updates the given workspace with the given name. Returns a query result
|
||||||
// query result with status 200 on success.
|
// with status 200 on success.
|
||||||
public async updateWorkspace(scope: Scope, wsId: number,
|
public async updateWorkspace(
|
||||||
props: Partial<WorkspaceProperties>): Promise<QueryResult<number>> {
|
scope: Scope,
|
||||||
|
wsId: number,
|
||||||
|
props: Partial<WorkspaceProperties>
|
||||||
|
): Promise<QueryResult<PreviousAndCurrent<Workspace>>> {
|
||||||
return await this._connection.transaction(async manager => {
|
return await this._connection.transaction(async manager => {
|
||||||
const wsQuery = this._workspace(scope, wsId, {
|
const wsQuery = this._workspace(scope, wsId, {
|
||||||
manager,
|
manager,
|
||||||
@ -1438,17 +1452,18 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
// Update the name and save.
|
// Update the name and save.
|
||||||
const workspace: Workspace = queryResult.data;
|
const workspace: Workspace = queryResult.data;
|
||||||
|
const previous = structuredClone(workspace);
|
||||||
workspace.checkProperties(props);
|
workspace.checkProperties(props);
|
||||||
workspace.updateFromProperties(props);
|
workspace.updateFromProperties(props);
|
||||||
await manager.save(workspace);
|
await manager.save(workspace);
|
||||||
return {status: 200};
|
return {status: 200, data: {previous, current: workspace}};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the user has REMOVE permissions to the given workspace. If not, throws an
|
// Checks that the user has REMOVE permissions to the given workspace. If not, throws an
|
||||||
// error. Otherwise deletes the given workspace. Returns an empty query result with
|
// error. Otherwise deletes the given workspace. Returns a query result with status 200
|
||||||
// status 200 on success.
|
// on success.
|
||||||
public async deleteWorkspace(scope: Scope, wsId: number): Promise<QueryResult<number>> {
|
public async deleteWorkspace(scope: Scope, wsId: number): Promise<QueryResult<Workspace>> {
|
||||||
return await this._connection.transaction(async manager => {
|
return await this._connection.transaction(async manager => {
|
||||||
const wsQuery = this._workspace(scope, wsId, {
|
const wsQuery = this._workspace(scope, wsId, {
|
||||||
manager,
|
manager,
|
||||||
@ -1469,6 +1484,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
return queryResult;
|
return queryResult;
|
||||||
}
|
}
|
||||||
const workspace: Workspace = queryResult.data;
|
const workspace: Workspace = queryResult.data;
|
||||||
|
const deletedWorkspace = structuredClone(workspace);
|
||||||
// Delete the workspace, workspace docs, doc ACLs/groups and workspace ACLs/groups.
|
// Delete the workspace, workspace docs, doc ACLs/groups and workspace ACLs/groups.
|
||||||
const wsGroups = workspace.aclRules.map(wsAcl => wsAcl.group);
|
const wsGroups = workspace.aclRules.map(wsAcl => wsAcl.group);
|
||||||
const docAcls = ([] as AclRule[]).concat(...workspace.docs.map(doc => doc.aclRules));
|
const docAcls = ([] as AclRule[]).concat(...workspace.docs.map(doc => doc.aclRules));
|
||||||
@ -1477,15 +1493,15 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
...workspace.aclRules, ...docGroups]);
|
...workspace.aclRules, ...docGroups]);
|
||||||
// Update the guests in the org after removing this workspace.
|
// Update the guests in the org after removing this workspace.
|
||||||
await this._repairOrgGuests(scope, workspace.org.id, manager);
|
await this._repairOrgGuests(scope, workspace.org.id, manager);
|
||||||
return {status: 200};
|
return {status: 200, data: deletedWorkspace};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public softDeleteWorkspace(scope: Scope, wsId: number): Promise<void> {
|
public softDeleteWorkspace(scope: Scope, wsId: number): Promise<QueryResult<Workspace>> {
|
||||||
return this._setWorkspaceRemovedAt(scope, wsId, new Date());
|
return this._setWorkspaceRemovedAt(scope, wsId, new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async undeleteWorkspace(scope: Scope, wsId: number): Promise<void> {
|
public async undeleteWorkspace(scope: Scope, wsId: number): Promise<QueryResult<Workspace>> {
|
||||||
return this._setWorkspaceRemovedAt(scope, wsId, null);
|
return this._setWorkspaceRemovedAt(scope, wsId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1691,15 +1707,15 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Checks that the user has SCHEMA_EDIT permissions to the given doc. If not, throws an
|
// Checks that the user has SCHEMA_EDIT permissions to the given doc. If not, throws an
|
||||||
// error. Otherwise updates the given doc with the given name. Returns an empty
|
// error. Otherwise updates the given doc with the given name. Returns a query result with
|
||||||
// query result with status 200 on success.
|
// status 200 on success.
|
||||||
// NOTE: This does not update the updateAt date indicating the last modified time of the doc.
|
// NOTE: This does not update the updateAt date indicating the last modified time of the doc.
|
||||||
// We may want to make it do so.
|
// We may want to make it do so.
|
||||||
public async updateDocument(
|
public async updateDocument(
|
||||||
scope: DocScope,
|
scope: DocScope,
|
||||||
props: Partial<DocumentProperties>,
|
props: Partial<DocumentProperties>,
|
||||||
transaction?: EntityManager
|
transaction?: EntityManager
|
||||||
): Promise<QueryResult<number>> {
|
): Promise<QueryResult<PreviousAndCurrent<Document>>> {
|
||||||
const markPermissions = Permissions.SCHEMA_EDIT;
|
const markPermissions = Permissions.SCHEMA_EDIT;
|
||||||
return await this._runInTransaction(transaction, async (manager) => {
|
return await this._runInTransaction(transaction, async (manager) => {
|
||||||
const {forkId} = parseUrlId(scope.urlId);
|
const {forkId} = parseUrlId(scope.urlId);
|
||||||
@ -1721,6 +1737,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
// Update the name and save.
|
// Update the name and save.
|
||||||
const doc: Document = queryResult.data;
|
const doc: Document = queryResult.data;
|
||||||
|
const previous = structuredClone(doc);
|
||||||
doc.checkProperties(props);
|
doc.checkProperties(props);
|
||||||
doc.updateFromProperties(props);
|
doc.updateFromProperties(props);
|
||||||
if (forkId) {
|
if (forkId) {
|
||||||
@ -1752,7 +1769,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
.execute();
|
.execute();
|
||||||
// TODO: we could limit the max number of aliases stored per document.
|
// TODO: we could limit the max number of aliases stored per document.
|
||||||
}
|
}
|
||||||
return {status: 200};
|
return {status: 200, data: {previous, current: doc}};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1909,7 +1926,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
scope: Scope,
|
scope: Scope,
|
||||||
orgKey: string|number,
|
orgKey: string|number,
|
||||||
delta: PermissionDelta
|
delta: PermissionDelta
|
||||||
): Promise<QueryResult<void>> {
|
): Promise<QueryResult<PermissionDelta & {organization: Organization}>> {
|
||||||
const {userId} = scope;
|
const {userId} = scope;
|
||||||
const notifications: Array<() => void> = [];
|
const notifications: Array<() => void> = [];
|
||||||
const result = await this._connection.transaction(async manager => {
|
const result = await this._connection.transaction(async manager => {
|
||||||
@ -1955,7 +1972,10 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
// Notify any added users that they've been added to this resource.
|
// Notify any added users that they've been added to this resource.
|
||||||
notifications.push(this._inviteNotification(userId, org, userIdDelta, membersBefore));
|
notifications.push(this._inviteNotification(userId, org, userIdDelta, membersBefore));
|
||||||
}
|
}
|
||||||
return {status: 200};
|
return {status: 200, data: {
|
||||||
|
organization: org,
|
||||||
|
users: userIdDelta ?? undefined,
|
||||||
|
}};
|
||||||
});
|
});
|
||||||
for (const notification of notifications) { notification(); }
|
for (const notification of notifications) { notification(); }
|
||||||
return result;
|
return result;
|
||||||
@ -1966,7 +1986,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
scope: Scope,
|
scope: Scope,
|
||||||
wsId: number,
|
wsId: number,
|
||||||
delta: PermissionDelta
|
delta: PermissionDelta
|
||||||
): Promise<QueryResult<void>> {
|
): Promise<QueryResult<PermissionDelta & {workspace: Workspace}>> {
|
||||||
const {userId} = scope;
|
const {userId} = scope;
|
||||||
const notifications: Array<() => void> = [];
|
const notifications: Array<() => void> = [];
|
||||||
const result = await this._connection.transaction(async manager => {
|
const result = await this._connection.transaction(async manager => {
|
||||||
@ -2031,7 +2051,14 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
await this._repairOrgGuests(scope, ws.org.id, manager);
|
await this._repairOrgGuests(scope, ws.org.id, manager);
|
||||||
notifications.push(this._inviteNotification(userId, ws, userIdDelta, membersBefore));
|
notifications.push(this._inviteNotification(userId, ws, userIdDelta, membersBefore));
|
||||||
}
|
}
|
||||||
return {status: 200};
|
return {
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
workspace: ws,
|
||||||
|
maxInheritedRole: delta.maxInheritedRole,
|
||||||
|
users: userIdDelta ?? undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
for (const notification of notifications) { notification(); }
|
for (const notification of notifications) { notification(); }
|
||||||
return result;
|
return result;
|
||||||
@ -2041,7 +2068,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
public async updateDocPermissions(
|
public async updateDocPermissions(
|
||||||
scope: DocScope,
|
scope: DocScope,
|
||||||
delta: PermissionDelta
|
delta: PermissionDelta
|
||||||
): Promise<QueryResult<void>> {
|
): Promise<QueryResult<PermissionDelta & {document: Document}>> {
|
||||||
const notifications: Array<() => void> = [];
|
const notifications: Array<() => void> = [];
|
||||||
const result = await this._connection.transaction(async manager => {
|
const result = await this._connection.transaction(async manager => {
|
||||||
const {userId} = scope;
|
const {userId} = scope;
|
||||||
@ -2082,7 +2109,14 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
await this._repairOrgGuests(scope, doc.workspace.org.id, manager);
|
await this._repairOrgGuests(scope, doc.workspace.org.id, manager);
|
||||||
notifications.push(this._inviteNotification(userId, doc, userIdDelta, membersBefore));
|
notifications.push(this._inviteNotification(userId, doc, userIdDelta, membersBefore));
|
||||||
}
|
}
|
||||||
return {status: 200};
|
return {
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
document: doc,
|
||||||
|
maxInheritedRole: delta.maxInheritedRole,
|
||||||
|
users: userIdDelta ?? undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
for (const notification of notifications) { notification(); }
|
for (const notification of notifications) { notification(); }
|
||||||
return result;
|
return result;
|
||||||
@ -2386,7 +2420,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
public async pinDoc(
|
public async pinDoc(
|
||||||
scope: DocScope,
|
scope: DocScope,
|
||||||
setPinned: boolean
|
setPinned: boolean
|
||||||
): Promise<QueryResult<void>> {
|
): Promise<QueryResult<Document>> {
|
||||||
return await this._connection.transaction(async manager => {
|
return await this._connection.transaction(async manager => {
|
||||||
// Find the doc to assert that it exists. Assert that the user has edit access to the
|
// Find the doc to assert that it exists. Assert that the user has edit access to the
|
||||||
// parent org.
|
// parent org.
|
||||||
@ -2410,7 +2444,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
// Save and return success status.
|
// Save and return success status.
|
||||||
await manager.save(doc);
|
await manager.save(doc);
|
||||||
}
|
}
|
||||||
return { status: 200 };
|
return {status: 200, data: doc};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4291,9 +4325,9 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
markPermissions: Permissions.REMOVE
|
markPermissions: Permissions.REMOVE
|
||||||
});
|
});
|
||||||
const workspace: Workspace = this.unwrapQueryResult(await verifyEntity(wsQuery));
|
const workspace: Workspace = this.unwrapQueryResult(await verifyEntity(wsQuery));
|
||||||
await manager.createQueryBuilder()
|
workspace.removedAt = removedAt;
|
||||||
.update(Workspace).set({removedAt}).where({id: workspace.id})
|
const data = await manager.save(workspace);
|
||||||
.execute();
|
return {status: 200, data};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,13 +257,16 @@ export class UsersManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updateUser(userId: number, props: UserProfileChange){
|
public async updateUser(userId: number, props: UserProfileChange){
|
||||||
let isWelcomed: boolean = false;
|
return await this._connection.transaction(async manager => {
|
||||||
let user: User|null = null;
|
let isWelcomed = false;
|
||||||
await this._connection.transaction(async manager => {
|
|
||||||
user = await manager.findOne(User, {relations: ['logins'],
|
|
||||||
where: {id: userId}});
|
|
||||||
let needsSave = false;
|
let needsSave = false;
|
||||||
|
const user = await manager.findOne(User, {
|
||||||
|
relations: ['logins'],
|
||||||
|
where: {id: userId},
|
||||||
|
});
|
||||||
if (!user) { throw new ApiError("unable to find user", 400); }
|
if (!user) { throw new ApiError("unable to find user", 400); }
|
||||||
|
|
||||||
|
const previous = structuredClone(user);
|
||||||
if (props.name && props.name !== user.name) {
|
if (props.name && props.name !== user.name) {
|
||||||
user.name = props.name;
|
user.name = props.name;
|
||||||
needsSave = true;
|
needsSave = true;
|
||||||
@ -279,8 +282,8 @@ export class UsersManager {
|
|||||||
if (needsSave) {
|
if (needsSave) {
|
||||||
await manager.save(user);
|
await manager.save(user);
|
||||||
}
|
}
|
||||||
|
return {previous, current: user, isWelcomed};
|
||||||
});
|
});
|
||||||
return { user, isWelcomed };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rather use the updateUser() method, if that makes sense?
|
// TODO: rather use the updateUser() method, if that makes sense?
|
||||||
@ -454,9 +457,9 @@ export class UsersManager {
|
|||||||
|
|
||||||
// We just created a personal org; set userOrgPrefs that should apply for new users only.
|
// We just created a personal org; set userOrgPrefs that should apply for new users only.
|
||||||
const userOrgPrefs: UserOrgPrefs = {showGristTour: true};
|
const userOrgPrefs: UserOrgPrefs = {showGristTour: true};
|
||||||
const orgId = result.data;
|
const org = result.data;
|
||||||
if (orgId) {
|
if (org) {
|
||||||
await this._homeDb.updateOrg({userId: user.id}, orgId, {userOrgPrefs}, manager);
|
await this._homeDb.updateOrg({userId: user.id}, org.id, {userOrgPrefs}, manager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (needUpdate) {
|
if (needUpdate) {
|
||||||
|
@ -9,6 +9,7 @@ import { getDatabaseUrl } from 'app/server/lib/serverUtils';
|
|||||||
import { getTelemetryPrefs } from 'app/server/lib/Telemetry';
|
import { getTelemetryPrefs } from 'app/server/lib/Telemetry';
|
||||||
import { Gristifier } from 'app/server/utils/gristify';
|
import { Gristifier } from 'app/server/utils/gristify';
|
||||||
import { pruneActionHistory } from 'app/server/utils/pruneActionHistory';
|
import { pruneActionHistory } from 'app/server/utils/pruneActionHistory';
|
||||||
|
import { showAuditLogEvents } from 'app/server/utils/showAuditLogEvents';
|
||||||
import * as commander from 'commander';
|
import * as commander from 'commander';
|
||||||
import { Connection } from 'typeorm';
|
import { Connection } from 'typeorm';
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ export function getProgram(): commander.Command {
|
|||||||
// want to reserve "grist" for electron app?
|
// want to reserve "grist" for electron app?
|
||||||
.description('a toolbox of handy Grist-related utilities');
|
.description('a toolbox of handy Grist-related utilities');
|
||||||
|
|
||||||
|
addAuditLogsCommand(program, {nested: true});
|
||||||
addDbCommand(program, {nested: true});
|
addDbCommand(program, {nested: true});
|
||||||
addHistoryCommand(program, {nested: true});
|
addHistoryCommand(program, {nested: true});
|
||||||
addSettingsCommand(program, {nested: true});
|
addSettingsCommand(program, {nested: true});
|
||||||
@ -52,6 +54,18 @@ export function getProgram(): commander.Command {
|
|||||||
return program;
|
return program;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addAuditLogsCommand(program: commander.Command, options: CommandOptions) {
|
||||||
|
const sub = section(program, {
|
||||||
|
sectionName: 'audit-logs',
|
||||||
|
sectionDescription: 'show information about audit logs',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
sub('events')
|
||||||
|
.description('show audit log events')
|
||||||
|
.addOption(new commander.Option('--type <type>').choices(['installation', 'site']))
|
||||||
|
.action(showAuditLogEvents);
|
||||||
|
}
|
||||||
|
|
||||||
// Add commands related to document history:
|
// Add commands related to document history:
|
||||||
// history prune <docId> [N]
|
// history prune <docId> [N]
|
||||||
export function addHistoryCommand(program: commander.Command, options: CommandOptions) {
|
export function addHistoryCommand(program: commander.Command, options: CommandOptions) {
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
import {ApiError} from 'app/common/ApiError';
|
import {ApiError} from 'app/common/ApiError';
|
||||||
import {mapGetOrSet, MapWithTTL} from 'app/common/AsyncCreate';
|
import {mapGetOrSet, MapWithTTL} from 'app/common/AsyncCreate';
|
||||||
import {AttachmentColumns, gatherAttachmentIds, getAttachmentColumns} from 'app/common/AttachmentColumns';
|
import {AttachmentColumns, gatherAttachmentIds, getAttachmentColumns} from 'app/common/AttachmentColumns';
|
||||||
|
import {AuditEventName} from 'app/common/AuditEvent';
|
||||||
import {WebhookMessageType} from 'app/common/CommTypes';
|
import {WebhookMessageType} from 'app/common/CommTypes';
|
||||||
import {
|
import {
|
||||||
BulkAddRecord,
|
BulkAddRecord,
|
||||||
@ -92,6 +93,7 @@ import {ParseFileResult, ParseOptions} from 'app/plugin/FileParserAPI';
|
|||||||
import {AccessTokenOptions, AccessTokenResult, GristDocAPI, UIRowId} from 'app/plugin/GristAPI';
|
import {AccessTokenOptions, AccessTokenResult, GristDocAPI, UIRowId} from 'app/plugin/GristAPI';
|
||||||
import {AssistanceSchemaPromptV1Context} from 'app/server/lib/Assistance';
|
import {AssistanceSchemaPromptV1Context} from 'app/server/lib/Assistance';
|
||||||
import {AssistanceContext} from 'app/common/AssistancePrompts';
|
import {AssistanceContext} from 'app/common/AssistancePrompts';
|
||||||
|
import {AuditEventProperties} from 'app/server/lib/AuditLogger';
|
||||||
import {Authorizer, RequestWithLogin} from 'app/server/lib/Authorizer';
|
import {Authorizer, RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||||
import {checksumFile} from 'app/server/lib/checksumFile';
|
import {checksumFile} from 'app/server/lib/checksumFile';
|
||||||
import {Client} from 'app/server/lib/Client';
|
import {Client} from 'app/server/lib/Client';
|
||||||
@ -115,6 +117,7 @@ import {
|
|||||||
getFullUser,
|
getFullUser,
|
||||||
getLogMeta,
|
getLogMeta,
|
||||||
getUserId,
|
getUserId,
|
||||||
|
RequestOrSession,
|
||||||
} from 'app/server/lib/sessionUtils';
|
} from 'app/server/lib/sessionUtils';
|
||||||
import {shortDesc} from 'app/server/lib/shortDesc';
|
import {shortDesc} from 'app/server/lib/shortDesc';
|
||||||
import {TableMetadataLoader} from 'app/server/lib/TableMetadataLoader';
|
import {TableMetadataLoader} from 'app/server/lib/TableMetadataLoader';
|
||||||
@ -1451,17 +1454,7 @@ export class ActiveDoc extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await dbManager.forkDoc(userId, doc, forkIds.forkId);
|
await dbManager.forkDoc(userId, doc, forkIds.forkId);
|
||||||
|
this._logForkDocumentEvents(docSession, {originalDocument: doc, forkIds});
|
||||||
const isTemplate = doc.type === 'template';
|
|
||||||
this.logTelemetryEvent(docSession, 'documentForked', {
|
|
||||||
limited: {
|
|
||||||
forkIdDigest: forkIds.forkId,
|
|
||||||
forkDocIdDigest: forkIds.docId,
|
|
||||||
trunkIdDigest: doc.trunkId,
|
|
||||||
isTemplate,
|
|
||||||
lastActivity: doc.updatedAt,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
await permitStore.removePermit(permitKey);
|
await permitStore.removePermit(permitKey);
|
||||||
}
|
}
|
||||||
@ -1865,6 +1858,13 @@ export class ActiveDoc extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public logAuditEvent<Name extends AuditEventName>(
|
||||||
|
requestOrSession: RequestOrSession,
|
||||||
|
properties: AuditEventProperties<Name>
|
||||||
|
) {
|
||||||
|
this._docManager.gristServer.getAuditLogger().logEvent(requestOrSession, properties);
|
||||||
|
}
|
||||||
|
|
||||||
public logTelemetryEvent(
|
public logTelemetryEvent(
|
||||||
docSession: OptDocSession | null,
|
docSession: OptDocSession | null,
|
||||||
event: TelemetryEvent,
|
event: TelemetryEvent,
|
||||||
@ -2961,6 +2961,38 @@ export class ActiveDoc extends EventEmitter {
|
|||||||
return this._pyCall('start_timing');
|
return this._pyCall('start_timing');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _logForkDocumentEvents(docSession: OptDocSession, options: {
|
||||||
|
originalDocument: Document;
|
||||||
|
forkIds: ForkResult;
|
||||||
|
}) {
|
||||||
|
const {originalDocument, forkIds} = options;
|
||||||
|
this.logAuditEvent(docSession, {
|
||||||
|
event: {
|
||||||
|
name: 'forkDocument',
|
||||||
|
details: {
|
||||||
|
original: {
|
||||||
|
id: originalDocument.id,
|
||||||
|
name: originalDocument.name,
|
||||||
|
},
|
||||||
|
fork: {
|
||||||
|
id: forkIds.forkId,
|
||||||
|
documentId: forkIds.docId,
|
||||||
|
urlId: forkIds.urlId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {documentId: originalDocument.id},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logTelemetryEvent(docSession, 'documentForked', {
|
||||||
|
limited: {
|
||||||
|
forkIdDigest: forkIds.forkId,
|
||||||
|
forkDocIdDigest: forkIds.docId,
|
||||||
|
trunkIdDigest: originalDocument.trunkId,
|
||||||
|
isTemplate: originalDocument.type === 'template',
|
||||||
|
lastActivity: originalDocument.updatedAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to initialize a sandbox action bundle with no values.
|
// Helper to initialize a sandbox action bundle with no values.
|
||||||
|
@ -153,30 +153,8 @@ export function attachAppEndpoint(options: AttachOptions): void {
|
|||||||
docStatus = workerInfo.docStatus;
|
docStatus = workerInfo.docStatus;
|
||||||
body = await workerInfo.resp.json();
|
body = await workerInfo.resp.json();
|
||||||
}
|
}
|
||||||
|
logOpenDocumentEvents(mreq, {server: gristServer, doc, urlId});
|
||||||
const isPublic = ((doc as unknown) as APIDocument).public ?? false;
|
if (doc.type === 'template') {
|
||||||
const isSnapshot = Boolean(parseUrlId(urlId).snapshotId);
|
|
||||||
const isTemplate = doc.type === 'template';
|
|
||||||
if (isPublic || isTemplate) {
|
|
||||||
gristServer.getTelemetry().logEvent(mreq, 'documentOpened', {
|
|
||||||
limited: {
|
|
||||||
docIdDigest: docId,
|
|
||||||
access: doc.access,
|
|
||||||
isPublic,
|
|
||||||
isSnapshot,
|
|
||||||
isTemplate,
|
|
||||||
lastUpdated: doc.updatedAt,
|
|
||||||
},
|
|
||||||
full: {
|
|
||||||
siteId: doc.workspace.org.id,
|
|
||||||
siteType: doc.workspace.org.billingAccount.product.name,
|
|
||||||
userId: mreq.userId,
|
|
||||||
altSessionId: mreq.altSessionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTemplate) {
|
|
||||||
// Keep track of the last template a user visited in the last hour.
|
// Keep track of the last template a user visited in the last hour.
|
||||||
// If a sign-up occurs within that time period, we'll know which
|
// If a sign-up occurs within that time period, we'll know which
|
||||||
// template, if any, was viewed most recently.
|
// template, if any, was viewed most recently.
|
||||||
@ -232,3 +210,39 @@ export function attachAppEndpoint(options: AttachOptions): void {
|
|||||||
app.get('/:urlId([^-/]{12,})(/:slug([^/]+):remainder(*))?',
|
app.get('/:urlId([^-/]{12,})(/:slug([^/]+):remainder(*))?',
|
||||||
...docMiddleware, docHandler);
|
...docMiddleware, docHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logOpenDocumentEvents(req: RequestWithLogin, options: {
|
||||||
|
server: GristServer;
|
||||||
|
doc: Document;
|
||||||
|
urlId: string;
|
||||||
|
}) {
|
||||||
|
const {server, doc, urlId} = options;
|
||||||
|
const {forkId, snapshotId} = parseUrlId(urlId);
|
||||||
|
server.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'openDocument',
|
||||||
|
details: {id: doc.id, name: doc.name, urlId, forkId, snapshotId},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPublic = ((doc as unknown) as APIDocument).public ?? false;
|
||||||
|
const isTemplate = doc.type === 'template';
|
||||||
|
if (isPublic || isTemplate) {
|
||||||
|
server.getTelemetry().logEvent(req, 'documentOpened', {
|
||||||
|
limited: {
|
||||||
|
docIdDigest: doc.id,
|
||||||
|
access: doc.access,
|
||||||
|
isPublic,
|
||||||
|
isSnapshot: Boolean(snapshotId),
|
||||||
|
isTemplate,
|
||||||
|
lastUpdated: doc.updatedAt,
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
siteId: doc.workspace.org.id,
|
||||||
|
siteType: doc.workspace.org.billingAccount.product.name,
|
||||||
|
userId: req.userId,
|
||||||
|
altSessionId: req.altSessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {AuditEvent, AuditEventContext, AuditEventDetails, AuditEventName} from 'app/common/AuditEvent';
|
import {AuditEvent, AuditEventContext, AuditEventDetails, AuditEventName, AuditEventUser} from 'app/common/AuditEvent';
|
||||||
import {RequestOrSession} from 'app/server/lib/sessionUtils';
|
import {RequestOrSession} from 'app/server/lib/sessionUtils';
|
||||||
|
|
||||||
export interface IAuditLogger {
|
export interface IAuditLogger {
|
||||||
@ -23,20 +23,24 @@ export interface IAuditLogger {
|
|||||||
export interface AuditEventProperties<Name extends AuditEventName> {
|
export interface AuditEventProperties<Name extends AuditEventName> {
|
||||||
event: {
|
event: {
|
||||||
/**
|
/**
|
||||||
* The event name.
|
* The name of the event.
|
||||||
*/
|
*/
|
||||||
name: Name;
|
name: Name;
|
||||||
/**
|
/**
|
||||||
* Additional event details.
|
* Event-specific details (e.g. properties of affected resources).
|
||||||
*/
|
*/
|
||||||
details?: AuditEventDetails[Name];
|
details?: AuditEventDetails[Name];
|
||||||
/**
|
/**
|
||||||
* The context of the event.
|
* The context that the event occurred in (e.g. workspace, document).
|
||||||
*/
|
*/
|
||||||
context?: AuditEventContext;
|
context?: AuditEventContext;
|
||||||
|
/**
|
||||||
|
* The user that triggered the event.
|
||||||
|
*/
|
||||||
|
user?: AuditEventUser;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* ISO 8601 timestamp (e.g. `2024-09-04T14:54:50Z`) of when the event occured.
|
* ISO 8601 timestamp (e.g. `2024-09-04T14:54:50Z`) of when the event occurred.
|
||||||
*
|
*
|
||||||
* Defaults to now.
|
* Defaults to now.
|
||||||
*/
|
*/
|
||||||
|
@ -906,8 +906,10 @@ export class DocWorkerApi {
|
|||||||
// Clears all outgoing webhooks in the queue for this document.
|
// Clears all outgoing webhooks in the queue for this document.
|
||||||
this._app.delete('/api/docs/:docId/webhooks/queue', isOwner,
|
this._app.delete('/api/docs/:docId/webhooks/queue', isOwner,
|
||||||
withDocTriggersLock(async (activeDoc, req, res) => {
|
withDocTriggersLock(async (activeDoc, req, res) => {
|
||||||
|
const docId = getDocId(req);
|
||||||
await activeDoc.clearWebhookQueue();
|
await activeDoc.clearWebhookQueue();
|
||||||
await activeDoc.sendWebhookNotification();
|
await activeDoc.sendWebhookNotification();
|
||||||
|
this._logClearAllWebhookQueueEvents(req, {docId});
|
||||||
res.json({success: true});
|
res.json({success: true});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -933,7 +935,7 @@ export class DocWorkerApi {
|
|||||||
const webhookId = req.params.webhookId;
|
const webhookId = req.params.webhookId;
|
||||||
const {fields, url, authorization} = await getWebhookSettings(activeDoc, req, webhookId, req.body);
|
const {fields, url, authorization} = await getWebhookSettings(activeDoc, req, webhookId, req.body);
|
||||||
if (fields.enabled === false) {
|
if (fields.enabled === false) {
|
||||||
await activeDoc.triggers.clearSingleWebhookQueue(webhookId);
|
await activeDoc.clearSingleWebhookQueue(webhookId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerRowId = activeDoc.triggers.getWebhookTriggerRecord(webhookId).id;
|
const triggerRowId = activeDoc.triggers.getWebhookTriggerRecord(webhookId).id;
|
||||||
@ -960,9 +962,11 @@ export class DocWorkerApi {
|
|||||||
// Clears a single webhook in the queue for this document.
|
// Clears a single webhook in the queue for this document.
|
||||||
this._app.delete('/api/docs/:docId/webhooks/queue/:webhookId', isOwner,
|
this._app.delete('/api/docs/:docId/webhooks/queue/:webhookId', isOwner,
|
||||||
withDocTriggersLock(async (activeDoc, req, res) => {
|
withDocTriggersLock(async (activeDoc, req, res) => {
|
||||||
|
const docId = getDocId(req);
|
||||||
const webhookId = req.params.webhookId;
|
const webhookId = req.params.webhookId;
|
||||||
await activeDoc.clearSingleWebhookQueue(webhookId);
|
await activeDoc.clearSingleWebhookQueue(webhookId);
|
||||||
await activeDoc.sendWebhookNotification();
|
await activeDoc.sendWebhookNotification();
|
||||||
|
this._logClearWebhookQueueEvents(req, {docId, webhookId});
|
||||||
res.json({success: true});
|
res.json({success: true});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -978,8 +982,10 @@ export class DocWorkerApi {
|
|||||||
// reopened on use).
|
// reopened on use).
|
||||||
this._app.post('/api/docs/:docId/force-reload', canEdit, async (req, res) => {
|
this._app.post('/api/docs/:docId/force-reload', canEdit, async (req, res) => {
|
||||||
const mreq = req as RequestWithLogin;
|
const mreq = req as RequestWithLogin;
|
||||||
|
const docId = getDocId(req);
|
||||||
const activeDoc = await this._getActiveDoc(mreq);
|
const activeDoc = await this._getActiveDoc(mreq);
|
||||||
await activeDoc.reloadDoc();
|
await activeDoc.reloadDoc();
|
||||||
|
this._logReloadDocumentEvents(mreq, {docId});
|
||||||
res.json(null);
|
res.json(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -997,16 +1003,16 @@ export class DocWorkerApi {
|
|||||||
// DELETE /api/docs/:docId
|
// DELETE /api/docs/:docId
|
||||||
// Delete the specified doc.
|
// Delete the specified doc.
|
||||||
this._app.delete('/api/docs/:docId', canEditMaybeRemoved, throttled(async (req, res) => {
|
this._app.delete('/api/docs/:docId', canEditMaybeRemoved, throttled(async (req, res) => {
|
||||||
const {status, data} = await this._removeDoc(req, res, true);
|
const {data} = await this._removeDoc(req, res, true);
|
||||||
if (status === 200) { this._logDeleteDocumentEvents(req, data!); }
|
if (data) { this._logDeleteDocumentEvents(req, data); }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// POST /api/docs/:docId/remove
|
// POST /api/docs/:docId/remove
|
||||||
// Soft-delete the specified doc. If query parameter "permanent" is set,
|
// Soft-delete the specified doc. If query parameter "permanent" is set,
|
||||||
// delete permanently.
|
// delete permanently.
|
||||||
this._app.post('/api/docs/:docId/remove', canEditMaybeRemoved, throttled(async (req, res) => {
|
this._app.post('/api/docs/:docId/remove', canEditMaybeRemoved, throttled(async (req, res) => {
|
||||||
const {status, data} = await this._removeDoc(req, res, isParameterOn(req.query.permanent));
|
const {data} = await this._removeDoc(req, res, isParameterOn(req.query.permanent));
|
||||||
if (status === 200) { this._logRemoveDocumentEvents(req, data!); }
|
if (data) { this._logRemoveDocumentEvents(req, data); }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this._app.get('/api/docs/:docId/snapshots', canView, withDoc(async (activeDoc, req, res) => {
|
this._app.get('/api/docs/:docId/snapshots', canView, withDoc(async (activeDoc, req, res) => {
|
||||||
@ -1100,6 +1106,7 @@ export class DocWorkerApi {
|
|||||||
// This endpoint cannot use withDoc since it is expected behavior for the ActiveDoc it
|
// This endpoint cannot use withDoc since it is expected behavior for the ActiveDoc it
|
||||||
// starts with to become muted.
|
// starts with to become muted.
|
||||||
this._app.post('/api/docs/:docId/replace', canEdit, throttled(async (req, res) => {
|
this._app.post('/api/docs/:docId/replace', canEdit, throttled(async (req, res) => {
|
||||||
|
const docId = getDocId(req);
|
||||||
const docSession = docSessionFromRequest(req);
|
const docSession = docSessionFromRequest(req);
|
||||||
const activeDoc = await this._getActiveDoc(req);
|
const activeDoc = await this._getActiveDoc(req);
|
||||||
const options: DocReplacementOptions = {};
|
const options: DocReplacementOptions = {};
|
||||||
@ -1160,6 +1167,9 @@ export class DocWorkerApi {
|
|||||||
options.snapshotId = String(req.body.snapshotId);
|
options.snapshotId = String(req.body.snapshotId);
|
||||||
}
|
}
|
||||||
await activeDoc.replace(docSession, options);
|
await activeDoc.replace(docSession, options);
|
||||||
|
const previous = {id: docId};
|
||||||
|
const current = {id: options.sourceDocId || docId, snapshotId: options.snapshotId};
|
||||||
|
this._logReplaceDocumentEvents(req, {previous, current});
|
||||||
res.json(null);
|
res.json(null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -1169,9 +1179,12 @@ export class DocWorkerApi {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this._app.post('/api/docs/:docId/states/remove', isOwner, withDoc(async (activeDoc, req, res) => {
|
this._app.post('/api/docs/:docId/states/remove', isOwner, withDoc(async (activeDoc, req, res) => {
|
||||||
|
const docId = getDocId(req);
|
||||||
const docSession = docSessionFromRequest(req);
|
const docSession = docSessionFromRequest(req);
|
||||||
const keep = integerParam(req.body.keep, 'keep');
|
const keep = integerParam(req.body.keep, 'keep');
|
||||||
res.json(await activeDoc.deleteActions(docSession, keep));
|
await activeDoc.deleteActions(docSession, keep);
|
||||||
|
this._logTruncateDocumentHistoryEvents(req, {docId, keep});
|
||||||
|
res.json(null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this._app.get('/api/docs/:docId/compare/:docId2', canView, withDoc(async (activeDoc, req, res) => {
|
this._app.get('/api/docs/:docId/compare/:docId2', canView, withDoc(async (activeDoc, req, res) => {
|
||||||
@ -1675,7 +1688,11 @@ export class DocWorkerApi {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this._logDuplicateDocumentEvents(mreq, {id: sourceDocumentId}, {id, name})
|
this._logDuplicateDocumentEvents(mreq, {
|
||||||
|
originalDocument: {id: sourceDocumentId},
|
||||||
|
duplicateDocument: {id, name},
|
||||||
|
asTemplate,
|
||||||
|
})
|
||||||
.catch(e => log.error('DocApi failed to log duplicate document events', e));
|
.catch(e => log.error('DocApi failed to log duplicate document events', e));
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@ -2029,8 +2046,13 @@ export class DocWorkerApi {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _runSql(activeDoc: ActiveDoc, req: RequestWithLogin, res: Response,
|
private async _runSql(
|
||||||
options: Types.SqlPost) {
|
activeDoc: ActiveDoc,
|
||||||
|
req: RequestWithLogin,
|
||||||
|
res: Response,
|
||||||
|
options: Types.SqlPost
|
||||||
|
) {
|
||||||
|
const docId = getDocId(req);
|
||||||
if (!await activeDoc.canCopyEverything(docSessionFromRequest(req))) {
|
if (!await activeDoc.canCopyEverything(docSessionFromRequest(req))) {
|
||||||
throw new ApiError('insufficient document access', 403);
|
throw new ApiError('insufficient document access', 403);
|
||||||
}
|
}
|
||||||
@ -2071,7 +2093,7 @@ export class DocWorkerApi {
|
|||||||
try {
|
try {
|
||||||
const records = await activeDoc.docStorage.all(wrappedStatement,
|
const records = await activeDoc.docStorage.all(wrappedStatement,
|
||||||
...(options.args || []));
|
...(options.args || []));
|
||||||
this._logRunSQLQueryEvents(req, options);
|
this._logRunSQLQueryEvents(req, {docId, ...options});
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
statement,
|
statement,
|
||||||
records: records.map(
|
records: records.map(
|
||||||
@ -2124,13 +2146,6 @@ export class DocWorkerApi {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
this._grist.getTelemetry().logEvent(mreq, 'createdDoc-Empty', {
|
this._grist.getTelemetry().logEvent(mreq, 'createdDoc-Empty', {
|
||||||
limited: {
|
|
||||||
docIdDigest: id,
|
|
||||||
sourceDocIdDigest: undefined,
|
|
||||||
isImport: false,
|
|
||||||
fileType: undefined,
|
|
||||||
isSaved: workspaceId !== undefined,
|
|
||||||
},
|
|
||||||
full: {
|
full: {
|
||||||
docIdDigest: id,
|
docIdDigest: id,
|
||||||
userId: mreq.userId,
|
userId: mreq.userId,
|
||||||
@ -2179,17 +2194,64 @@ export class DocWorkerApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _logDuplicateDocumentEvents(
|
private _logReplaceDocumentEvents(req: RequestWithLogin, options: {
|
||||||
req: RequestWithLogin,
|
previous: {id: string};
|
||||||
originalDocument: {id: string},
|
current: {id: string; snapshotId?: string};
|
||||||
newDocument: {id: string; name: string}
|
}) {
|
||||||
) {
|
const {previous, current} = options;
|
||||||
const document = await this._dbManager.getRawDocById(originalDocument.id);
|
this._grist.getAuditLogger().logEvent(req, {
|
||||||
const isTemplateCopy = document.type === 'template';
|
event: {
|
||||||
|
name: 'replaceDocument',
|
||||||
|
details: {
|
||||||
|
previous: {
|
||||||
|
id: previous.id,
|
||||||
|
},
|
||||||
|
current: {
|
||||||
|
id: current.id,
|
||||||
|
snapshotId: current.snapshotId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {documentId: previous.id},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _logDuplicateDocumentEvents(req: RequestWithLogin, options: {
|
||||||
|
originalDocument: {id: string};
|
||||||
|
duplicateDocument: {id: string; name: string};
|
||||||
|
asTemplate: boolean;
|
||||||
|
}) {
|
||||||
|
const {originalDocument: {id}, duplicateDocument, asTemplate} = options;
|
||||||
|
const originalDocument = await this._dbManager.getRawDocById(id);
|
||||||
|
this._grist.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'duplicateDocument',
|
||||||
|
details: {
|
||||||
|
original: {
|
||||||
|
id: originalDocument.id,
|
||||||
|
name: originalDocument.name,
|
||||||
|
workspace: {
|
||||||
|
id: originalDocument.workspace.id,
|
||||||
|
name: originalDocument.workspace.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duplicate: {
|
||||||
|
id: duplicateDocument.id,
|
||||||
|
name: duplicateDocument.name,
|
||||||
|
},
|
||||||
|
asTemplate,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
workspaceId: originalDocument.workspace.id,
|
||||||
|
documentId: originalDocument.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const isTemplateCopy = originalDocument.type === 'template';
|
||||||
if (isTemplateCopy) {
|
if (isTemplateCopy) {
|
||||||
this._grist.getTelemetry().logEvent(req, 'copiedTemplate', {
|
this._grist.getTelemetry().logEvent(req, 'copiedTemplate', {
|
||||||
full: {
|
full: {
|
||||||
templateId: parseUrlId(document.urlId || document.id).trunkId,
|
templateId: parseUrlId(originalDocument.urlId || originalDocument.id).trunkId,
|
||||||
userId: req.userId,
|
userId: req.userId,
|
||||||
altSessionId: req.altSessionId,
|
altSessionId: req.altSessionId,
|
||||||
},
|
},
|
||||||
@ -2200,7 +2262,7 @@ export class DocWorkerApi {
|
|||||||
`createdDoc-${isTemplateCopy ? 'CopyTemplate' : 'CopyDoc'}`,
|
`createdDoc-${isTemplateCopy ? 'CopyTemplate' : 'CopyDoc'}`,
|
||||||
{
|
{
|
||||||
full: {
|
full: {
|
||||||
docIdDigest: newDocument.id,
|
docIdDigest: duplicateDocument.id,
|
||||||
userId: req.userId,
|
userId: req.userId,
|
||||||
altSessionId: req.altSessionId,
|
altSessionId: req.altSessionId,
|
||||||
},
|
},
|
||||||
@ -2208,15 +2270,60 @@ export class DocWorkerApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _logRunSQLQueryEvents(
|
private _logReloadDocumentEvents(req: RequestWithLogin, {docId: documentId}: {docId: string}) {
|
||||||
|
this._grist.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'reloadDocument',
|
||||||
|
context: {documentId},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logTruncateDocumentHistoryEvents(
|
||||||
req: RequestWithLogin,
|
req: RequestWithLogin,
|
||||||
{sql: query, args, timeout}: Types.SqlPost
|
{docId: documentId, keep}: {docId: string; keep: number}
|
||||||
) {
|
) {
|
||||||
|
this._grist.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'truncateDocumentHistory',
|
||||||
|
details: {keep},
|
||||||
|
context: {documentId},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logClearWebhookQueueEvents(
|
||||||
|
req: RequestWithLogin,
|
||||||
|
{docId: documentId, webhookId: id}: {docId: string; webhookId: string}
|
||||||
|
) {
|
||||||
|
this._grist.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'clearWebhookQueue',
|
||||||
|
details: {id},
|
||||||
|
context: {documentId},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logClearAllWebhookQueueEvents(
|
||||||
|
req: RequestWithLogin,
|
||||||
|
{docId: documentId}: {docId: string}
|
||||||
|
) {
|
||||||
|
this._grist.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'clearAllWebhookQueues',
|
||||||
|
context: {documentId},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logRunSQLQueryEvents(req: RequestWithLogin, options: {docId: string} & Types.SqlPost) {
|
||||||
|
const {docId: documentId, sql: query, args, timeout: timeoutMs} = options;
|
||||||
this._grist.getAuditLogger().logEvent(req, {
|
this._grist.getAuditLogger().logEvent(req, {
|
||||||
event: {
|
event: {
|
||||||
name: 'runSQLQuery',
|
name: 'runSQLQuery',
|
||||||
details: {query, arguments: args, timeout},
|
details: {query, arguments: args, timeoutMs},
|
||||||
context: {documentId: getDocId(req)},
|
context: {documentId},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1492,7 +1492,7 @@ export class FlexServer implements GristServer {
|
|||||||
// to other (not public) team sites.
|
// to other (not public) team sites.
|
||||||
const doom = await createDoom();
|
const doom = await createDoom();
|
||||||
await doom.deleteUser(userId);
|
await doom.deleteUser(userId);
|
||||||
this.getTelemetry().logEvent(req as RequestWithLogin, 'deletedAccount');
|
this._logDeleteUserEvents(req as RequestWithLogin);
|
||||||
return resp.status(200).json(true);
|
return resp.status(200).json(true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -1523,16 +1523,10 @@ export class FlexServer implements GristServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reuse Doom cli tool for org deletion. Note, this removes everything as a super user.
|
// Reuse Doom cli tool for org deletion. Note, this removes everything as a super user.
|
||||||
|
const deletedOrg = structuredClone(org);
|
||||||
const doom = await createDoom();
|
const doom = await createDoom();
|
||||||
await doom.deleteOrg(org.id);
|
await doom.deleteOrg(org.id);
|
||||||
|
this._logDeleteSiteEvents(mreq, deletedOrg);
|
||||||
this.getTelemetry().logEvent(req as RequestWithLogin, 'deletedSite', {
|
|
||||||
full: {
|
|
||||||
siteId: org.id,
|
|
||||||
userId: mreq.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp.status(200).send();
|
return resp.status(200).send();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -2548,6 +2542,30 @@ export class FlexServer implements GristServer {
|
|||||||
|
|
||||||
return isGristLogHttpEnabled || deprecatedOptionEnablesLog;
|
return isGristLogHttpEnabled || deprecatedOptionEnablesLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _logDeleteUserEvents(req: RequestWithLogin) {
|
||||||
|
this.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'deleteUser',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.getTelemetry().logEvent(req, 'deletedAccount');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logDeleteSiteEvents(req: RequestWithLogin, {id, name}: Organization) {
|
||||||
|
this.getAuditLogger().logEvent(req, {
|
||||||
|
event: {
|
||||||
|
name: 'deleteSite',
|
||||||
|
details: {id, name},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.getTelemetry().logEvent(req, 'deletedSite', {
|
||||||
|
full: {
|
||||||
|
siteId: id,
|
||||||
|
userId: req.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,7 +3,7 @@ import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
|||||||
import {RequestWithLogin} from 'app/server/lib/Authorizer';
|
import {RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||||
import {streamXLSX} from 'app/server/lib/ExportXLSX';
|
import {streamXLSX} from 'app/server/lib/ExportXLSX';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
import {optStringParam} from 'app/server/lib/requestUtils';
|
import {getDocId, optStringParam} from 'app/server/lib/requestUtils';
|
||||||
import {Request, Response} from 'express';
|
import {Request, Response} from 'express';
|
||||||
import {PassThrough, Stream} from 'stream';
|
import {PassThrough, Stream} from 'stream';
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ export async function exportToDrive(
|
|||||||
throw new Error("No access token - Can't send file to Google Drive");
|
throw new Error("No access token - Can't send file to Google Drive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const docId = getDocId(req);
|
||||||
const mreq = req as RequestWithLogin;
|
const mreq = req as RequestWithLogin;
|
||||||
const meta = {
|
const meta = {
|
||||||
docId: activeDoc.docName,
|
docId: activeDoc.docName,
|
||||||
@ -39,6 +40,13 @@ export async function exportToDrive(
|
|||||||
streamXLSX(activeDoc, req, stream, {tableId: ''}),
|
streamXLSX(activeDoc, req, stream, {tableId: ''}),
|
||||||
sendFileToDrive(name, stream, access_token),
|
sendFileToDrive(name, stream, access_token),
|
||||||
]);
|
]);
|
||||||
|
activeDoc.logAuditEvent(mreq, {
|
||||||
|
event: {
|
||||||
|
name: 'sendToGoogleDrive',
|
||||||
|
details: {id: docId},
|
||||||
|
context: {documentId: docId},
|
||||||
|
},
|
||||||
|
});
|
||||||
log.debug(`Export to drive - File exported, redirecting to Google Spreadsheet ${url}`, meta);
|
log.debug(`Export to drive - File exported, redirecting to Google Spreadsheet ${url}`, meta);
|
||||||
res.json({ url });
|
res.json({ url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -691,17 +691,16 @@ export class DocTriggers {
|
|||||||
if (this._loopAbort.signal.aborted) {
|
if (this._loopAbort.signal.aborted) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let meta: Record<string, any>|undefined;
|
let meta: {webhookId: string; host: string, quantity: number} | undefined;
|
||||||
|
|
||||||
let success: boolean;
|
let success: boolean;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
success = true;
|
success = true;
|
||||||
} else {
|
} else {
|
||||||
await this._stats.logStatus(id, 'sending');
|
await this._stats.logStatus(id, 'sending');
|
||||||
meta = {numEvents: batch.length, webhookId: id, host: new URL(url).host};
|
meta = {webhookId: id, host: new URL(url).host, quantity: batch.length};
|
||||||
this._log("Sending batch of webhook events", meta);
|
this._log("Sending batch of webhook events", meta);
|
||||||
this._activeDoc.logTelemetryEvent(null, 'sendingWebhooks', {
|
this._activeDoc.logTelemetryEvent(null, 'sendingWebhooks', {
|
||||||
limited: {numEvents: meta.numEvents},
|
limited: {numEvents: meta.quantity},
|
||||||
});
|
});
|
||||||
success = await this._sendWebhookWithRetries(
|
success = await this._sendWebhookWithRetries(
|
||||||
id, url, authorization, body, batch.length, this._loopAbort.signal);
|
id, url, authorization, body, batch.length, this._loopAbort.signal);
|
||||||
@ -743,6 +742,14 @@ export class DocTriggers {
|
|||||||
await this._stats.logStatus(id, 'idle');
|
await this._stats.logStatus(id, 'idle');
|
||||||
if (meta) {
|
if (meta) {
|
||||||
this._log("Successfully sent batch of webhook events", meta);
|
this._log("Successfully sent batch of webhook events", meta);
|
||||||
|
const {webhookId, host, quantity} = meta;
|
||||||
|
this._activeDoc.logAuditEvent(null, {
|
||||||
|
event: {
|
||||||
|
name: 'deliverWebhookEvents',
|
||||||
|
details: {id: webhookId, host, quantity},
|
||||||
|
user: {type: 'system'},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
688
app/server/utils/showAuditLogEvents.ts
Normal file
688
app/server/utils/showAuditLogEvents.ts
Normal file
@ -0,0 +1,688 @@
|
|||||||
|
import {AuditEventDetails, AuditEventName, SiteAuditEventName} from 'app/common/AuditEvent';
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
/**
|
||||||
|
* The type of audit log events to show.
|
||||||
|
*
|
||||||
|
* Defaults to `"installation"`.
|
||||||
|
*/
|
||||||
|
type?: AuditEventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditEventType = 'installation' | 'site';
|
||||||
|
|
||||||
|
export function showAuditLogEvents({type = 'installation'}: Options) {
|
||||||
|
showTitle(type);
|
||||||
|
const events = getAuditEvents(type);
|
||||||
|
showTableOfContents(events);
|
||||||
|
showEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTitle(type: AuditEventType) {
|
||||||
|
if (type === 'installation') {
|
||||||
|
console.log('# Installation audit log events {: .tag-core .tag-ee }\n');
|
||||||
|
} else {
|
||||||
|
console.log('# Site audit log events\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuditEvents(type: AuditEventType): [string, AuditEvent<AuditEventName>][] {
|
||||||
|
if (type === 'installation') {
|
||||||
|
return Object.entries(AuditEvents).filter(([name]) => AuditEventName.guard(name));
|
||||||
|
} else {
|
||||||
|
return Object.entries(AuditEvents).filter(([name]) => SiteAuditEventName.guard(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTableOfContents(events: [string, AuditEvent<AuditEventName>][]) {
|
||||||
|
for (const [name] of events) {
|
||||||
|
console.log(` - [${name}](#${name.toLowerCase()})`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEvents(events: [string, AuditEvent<AuditEventName>][]) {
|
||||||
|
for (const [name, event] of events) {
|
||||||
|
const {description, properties} = event;
|
||||||
|
console.log(`## ${name}\n`);
|
||||||
|
console.log(`${description}\n`);
|
||||||
|
if (Object.keys(properties).length === 0) { continue; }
|
||||||
|
|
||||||
|
console.log('### Properties\n');
|
||||||
|
console.log('| Name | Type | Description |');
|
||||||
|
console.log('| ---- | ---- | ----------- |');
|
||||||
|
showEventProperties(properties);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEventProperties(
|
||||||
|
properties: AuditEventProperties<object>,
|
||||||
|
prefix = ''
|
||||||
|
) {
|
||||||
|
for (const [key, {type, description, optional, ...rest}] of Object.entries(properties)) {
|
||||||
|
const name = prefix + key + (optional ? ' *(optional)*' : '');
|
||||||
|
const types = (Array.isArray(type) ? type : [type]).map(t => `\`${t}\``);
|
||||||
|
console.log(`| ${name} | ${types.join(' or ')} | ${description} |`);
|
||||||
|
if ('properties' in rest) {
|
||||||
|
showEventProperties(rest.properties, prefix + `${name}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditEvents = {
|
||||||
|
[Name in keyof AuditEventDetails]: Name extends AuditEventName
|
||||||
|
? AuditEvent<Name>
|
||||||
|
: never
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuditEvent<Name extends AuditEventName> {
|
||||||
|
description: string;
|
||||||
|
properties: AuditEventProperties<AuditEventDetails[Name]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditEventProperties<T> = {
|
||||||
|
[K in keyof T]: T[K] extends object
|
||||||
|
? AuditEventProperty & {properties: AuditEventProperties<T[K]>}
|
||||||
|
: AuditEventProperty
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuditEventProperty {
|
||||||
|
type: string | string[];
|
||||||
|
description: string;
|
||||||
|
optional?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuditEvents: AuditEvents = {
|
||||||
|
createDocument: {
|
||||||
|
description: 'A new document was created.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sendToGoogleDrive: {
|
||||||
|
description: 'A document was sent to Google Drive.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renameDocument: {
|
||||||
|
description: 'A document was renamed.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
previousName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The previous name of the document.',
|
||||||
|
},
|
||||||
|
currentName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The current name of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pinDocument: {
|
||||||
|
description: 'A document was pinned.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unpinDocument: {
|
||||||
|
description: 'A document was unpinned.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
moveDocument: {
|
||||||
|
description: 'A document was moved to a new workspace.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
previousWorkspace: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The workspace the document was moved from.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newWorkspace: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The workspace the document was moved to.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
removeDocument: {
|
||||||
|
description: 'A document was moved to the trash.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deleteDocument: {
|
||||||
|
description: 'A document was permanently deleted.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
restoreDocumentFromTrash: {
|
||||||
|
description: 'A document was restored from the trash.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The workspace of the document.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changeDocumentAccess: {
|
||||||
|
description: 'Access to a document was changed.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The access level of the document.',
|
||||||
|
properties: {
|
||||||
|
maxInheritedRole: {
|
||||||
|
type: ['"owners"', '"editors"', '"viewers"', 'null'],
|
||||||
|
description: 'The max inherited role.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The access level by user ID.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
openDocument: {
|
||||||
|
description: 'A document was opened.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
urlId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The URL ID of the document.',
|
||||||
|
},
|
||||||
|
forkId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The fork ID of the document, if the document is a fork.',
|
||||||
|
},
|
||||||
|
snapshotId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The snapshot ID of the document, if the document is a snapshot.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duplicateDocument: {
|
||||||
|
description: 'A document was duplicated.',
|
||||||
|
properties: {
|
||||||
|
original: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The document that was duplicated.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The workspace of the document.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duplicate: {
|
||||||
|
description: 'The newly-duplicated document.',
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
asTemplate: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'If the document was duplicated without any data.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
forkDocument: {
|
||||||
|
description: 'A document was forked.',
|
||||||
|
properties: {
|
||||||
|
original: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The document that was forked.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fork: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The newly-forked document.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the fork.',
|
||||||
|
},
|
||||||
|
documentId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the fork with the trunk ID.',
|
||||||
|
},
|
||||||
|
urlId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the fork with the trunk URL ID.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
replaceDocument: {
|
||||||
|
description: 'A document was replaced.',
|
||||||
|
properties: {
|
||||||
|
previous: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The document that was replaced.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
current: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The newly-replaced document.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the document.',
|
||||||
|
},
|
||||||
|
snapshotId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the snapshot, if the document was replaced with one.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reloadDocument: {
|
||||||
|
description: 'A document was reloaded.',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
truncateDocumentHistory: {
|
||||||
|
description: "A document's history was truncated.",
|
||||||
|
properties: {
|
||||||
|
keep: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The number of history items kept.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deliverWebhookEvents: {
|
||||||
|
description: 'A batch of webhook events was delivered.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the webhook.',
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The host the webhook events were delivered to.',
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The number of webhook events delivered.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clearWebhookQueue: {
|
||||||
|
description: 'A webhook queue was cleared.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the webhook.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clearAllWebhookQueues: {
|
||||||
|
description: 'All webhook queues were cleared.',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
runSQLQuery: {
|
||||||
|
description: 'A SQL query was run on a document.',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The SQL query.'
|
||||||
|
},
|
||||||
|
arguments: {
|
||||||
|
type: 'Array<string | number>',
|
||||||
|
description: 'The arguments used for query parameters, if any.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
timeoutMs: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The query execution timeout duration in milliseconds.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createWorkspace: {
|
||||||
|
description: 'A new workspace was created.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renameWorkspace: {
|
||||||
|
description: 'A workspace was renamed.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
previousName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The previous name of the workspace.',
|
||||||
|
},
|
||||||
|
currentName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The current name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
removeWorkspace: {
|
||||||
|
description: 'A workspace was moved to the trash.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deleteWorkspace: {
|
||||||
|
description: 'A workspace was permanently deleted.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
restoreWorkspaceFromTrash: {
|
||||||
|
description: 'A workspace was restored from the trash.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the workspace.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changeWorkspaceAccess: {
|
||||||
|
description: 'Access to a workspace was changed.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the workspace.',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The access level of the workspace.',
|
||||||
|
properties: {
|
||||||
|
maxInheritedRole: {
|
||||||
|
type: ['"owners"', '"editors"', '"viewers"', 'null'],
|
||||||
|
description: 'The max inherited role.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The access level by user ID.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createSite: {
|
||||||
|
description: 'A new site was created.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the site.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the site.',
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The domain of the site.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renameSite: {
|
||||||
|
description: 'A site was renamed.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the site.',
|
||||||
|
},
|
||||||
|
previous: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The previous name and domain of the site.',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the site.',
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The domain of the site.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
current: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The current name and domain of the site.',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the site.',
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The domain of the site.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changeSiteAccess: {
|
||||||
|
description: 'Access to a site was changed.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the site.',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The access level of the site.',
|
||||||
|
properties: {
|
||||||
|
users: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'The access level by user ID.',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deleteSite: {
|
||||||
|
description: 'A site was deleted.',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The ID of the site.',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the site.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changeUserName: {
|
||||||
|
description: 'The name of a user was changed.',
|
||||||
|
properties: {
|
||||||
|
previousName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The previous name of the user.',
|
||||||
|
},
|
||||||
|
currentName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The current name of the user.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createUserAPIKey: {
|
||||||
|
description: 'A user API key was created.',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
deleteUserAPIKey: {
|
||||||
|
description: 'A user API key was deleted.',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
deleteUser: {
|
||||||
|
description: 'A user was deleted.',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
};
|
@ -90,7 +90,7 @@ describe('HomeDBManager', function() {
|
|||||||
|
|
||||||
it('can add an org', async function() {
|
it('can add an org', async function() {
|
||||||
const user = await home.getUserByLogin('chimpy@getgrist.com');
|
const user = await home.getUserByLogin('chimpy@getgrist.com');
|
||||||
const orgId = (await home.addOrg(user, {name: 'NewOrg', domain: 'novel-org'}, teamOptions)).data!;
|
const orgId = (await home.addOrg(user, {name: 'NewOrg', domain: 'novel-org'}, teamOptions)).data!.id;
|
||||||
const org = await home.getOrg({userId: user.id}, orgId);
|
const org = await home.getOrg({userId: user.id}, orgId);
|
||||||
assert.equal(org.data!.name, 'NewOrg');
|
assert.equal(org.data!.name, 'NewOrg');
|
||||||
assert.equal(org.data!.domain, 'novel-org');
|
assert.equal(org.data!.domain, 'novel-org');
|
||||||
@ -109,7 +109,7 @@ describe('HomeDBManager', function() {
|
|||||||
useNewPlan: true,
|
useNewPlan: true,
|
||||||
// omit plan, to use a default one (teamInitial)
|
// omit plan, to use a default one (teamInitial)
|
||||||
// it will either be 'stub' or anything set in GRIST_DEFAULT_PRODUCT
|
// it will either be 'stub' or anything set in GRIST_DEFAULT_PRODUCT
|
||||||
})).data!;
|
})).data!.id;
|
||||||
let org = await home.getOrg({userId: user.id}, orgId);
|
let org = await home.getOrg({userId: user.id}, orgId);
|
||||||
assert.equal(org.data!.name, 'NewOrg');
|
assert.equal(org.data!.name, 'NewOrg');
|
||||||
assert.equal(org.data!.domain, 'novel-org');
|
assert.equal(org.data!.domain, 'novel-org');
|
||||||
@ -121,7 +121,7 @@ describe('HomeDBManager', function() {
|
|||||||
orgId = (await home.addOrg(user, {name: 'NewOrg', domain: 'novel-org'}, {
|
orgId = (await home.addOrg(user, {name: 'NewOrg', domain: 'novel-org'}, {
|
||||||
setUserAsOwner: false,
|
setUserAsOwner: false,
|
||||||
useNewPlan: true,
|
useNewPlan: true,
|
||||||
})).data!;
|
})).data!.id;
|
||||||
|
|
||||||
org = await home.getOrg({userId: user.id}, orgId);
|
org = await home.getOrg({userId: user.id}, orgId);
|
||||||
assert.equal(org.data!.billingAccount.product.name, STUB_PLAN);
|
assert.equal(org.data!.billingAccount.product.name, STUB_PLAN);
|
||||||
@ -135,7 +135,7 @@ describe('HomeDBManager', function() {
|
|||||||
const user = await home.getUserByLogin('chimpy@getgrist.com');
|
const user = await home.getUserByLogin('chimpy@getgrist.com');
|
||||||
const domain = 'repeated-domain';
|
const domain = 'repeated-domain';
|
||||||
const result = await home.addOrg(user, {name: `${domain}!`, domain}, teamOptions);
|
const result = await home.addOrg(user, {name: `${domain}!`, domain}, teamOptions);
|
||||||
const orgId = result.data!;
|
const orgId = result.data!.id;
|
||||||
assert.equal(result.status, 200);
|
assert.equal(result.status, 200);
|
||||||
await assert.isRejected(home.addOrg(user, {name: `${domain}!`, domain}, teamOptions),
|
await assert.isRejected(home.addOrg(user, {name: `${domain}!`, domain}, teamOptions),
|
||||||
/Domain already in use/);
|
/Domain already in use/);
|
||||||
|
@ -45,7 +45,7 @@ describe('fixSiteProducts', function() {
|
|||||||
|
|
||||||
const productOrg = (id: number) => getOrg(id)?.then(org => org?.billingAccount?.product?.name);
|
const productOrg = (id: number) => getOrg(id)?.then(org => org?.billingAccount?.product?.name);
|
||||||
|
|
||||||
const freeOrgId = db.unwrapQueryResult(await db.addOrg(user, {
|
const freeOrg = db.unwrapQueryResult(await db.addOrg(user, {
|
||||||
name: org,
|
name: org,
|
||||||
domain: org,
|
domain: org,
|
||||||
}, {
|
}, {
|
||||||
@ -54,7 +54,7 @@ describe('fixSiteProducts', function() {
|
|||||||
product: 'teamFree',
|
product: 'teamFree',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const teamOrgId = db.unwrapQueryResult(await db.addOrg(user, {
|
const teamOrg = db.unwrapQueryResult(await db.addOrg(user, {
|
||||||
name: 'fix-team-org',
|
name: 'fix-team-org',
|
||||||
domain: 'fix-team-org',
|
domain: 'fix-team-org',
|
||||||
}, {
|
}, {
|
||||||
@ -64,7 +64,7 @@ describe('fixSiteProducts', function() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Make sure it is created with teamFree product.
|
// Make sure it is created with teamFree product.
|
||||||
assert.equal(await productOrg(freeOrgId), 'teamFree');
|
assert.equal(await productOrg(freeOrg.id), 'teamFree');
|
||||||
|
|
||||||
// Run the fixer.
|
// Run the fixer.
|
||||||
assert.isTrue(await fixSiteProducts({
|
assert.isTrue(await fixSiteProducts({
|
||||||
@ -73,10 +73,10 @@ describe('fixSiteProducts', function() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Make sure we fixed the product is on Free product.
|
// Make sure we fixed the product is on Free product.
|
||||||
assert.equal(await productOrg(freeOrgId), 'Free');
|
assert.equal(await productOrg(freeOrg.id), 'Free');
|
||||||
|
|
||||||
// Make sure the other org is still on team product.
|
// Make sure the other org is still on team product.
|
||||||
assert.equal(await productOrg(teamOrgId), 'team');
|
assert.equal(await productOrg(teamOrg.id), 'team');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't run when on saas deployment", async function() {
|
it("doesn't run when on saas deployment", async function() {
|
||||||
@ -123,7 +123,7 @@ describe('fixSiteProducts', function() {
|
|||||||
|
|
||||||
const db = server.dbManager;
|
const db = server.dbManager;
|
||||||
const user = await db.getUserByLogin(email, {profile});
|
const user = await db.getUserByLogin(email, {profile});
|
||||||
const orgId = db.unwrapQueryResult(await db.addOrg(user, {
|
const org = db.unwrapQueryResult(await db.addOrg(user, {
|
||||||
name: 'sanity-check-org',
|
name: 'sanity-check-org',
|
||||||
domain: 'sanity-check-org',
|
domain: 'sanity-check-org',
|
||||||
}, {
|
}, {
|
||||||
@ -135,12 +135,12 @@ describe('fixSiteProducts', function() {
|
|||||||
const getOrg = (id: number) => db.connection.manager.findOne(Organization,
|
const getOrg = (id: number) => db.connection.manager.findOne(Organization,
|
||||||
{where: {id}, relations: ['billingAccount', 'billingAccount.product']});
|
{where: {id}, relations: ['billingAccount', 'billingAccount.product']});
|
||||||
const productOrg = (id: number) => getOrg(id)?.then(org => org?.billingAccount?.product?.name);
|
const productOrg = (id: number) => getOrg(id)?.then(org => org?.billingAccount?.product?.name);
|
||||||
assert.equal(await productOrg(orgId), 'teamFree');
|
assert.equal(await productOrg(org.id), 'teamFree');
|
||||||
|
|
||||||
assert.isFalse(await fixSiteProducts({
|
assert.isFalse(await fixSiteProducts({
|
||||||
db: server.dbManager,
|
db: server.dbManager,
|
||||||
deploymentType: server.server.getDeploymentType(),
|
deploymentType: server.server.getDeploymentType(),
|
||||||
}));
|
}));
|
||||||
assert.equal(await productOrg(orgId), 'teamFree');
|
assert.equal(await productOrg(org.id), 'teamFree');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user