mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +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:
@@ -9,7 +9,9 @@ import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import {BasicRole} from 'app/common/roles';
|
||||
import {OrganizationProperties, PermissionDelta} from 'app/common/UserAPI';
|
||||
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 {Workspace} from 'app/gen-server/entity/Workspace';
|
||||
import {BillingOptions, HomeDBManager, Scope} from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||
import {PreviousAndCurrent, QueryResult} from 'app/gen-server/lib/homedb/Interfaces';
|
||||
import {getAuthorizedUserId, getUserId, getUserProfiles, RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||
@@ -77,7 +79,7 @@ export function addOrg(
|
||||
product?: string,
|
||||
billing?: BillingOptions,
|
||||
}
|
||||
): Promise<number> {
|
||||
): Promise<Organization> {
|
||||
return dbManager.connection.transaction(async manager => {
|
||||
const user = await manager.findOne(User, {where: {id: userId}});
|
||||
if (!user) { return handleDeletedUser(); }
|
||||
@@ -167,8 +169,9 @@ export class ApiServer {
|
||||
// doesn't have access to that information yet, so punting on this.
|
||||
// TODO: figure out who should be allowed to create organizations
|
||||
const userId = getAuthorizedUserId(req);
|
||||
const orgId = await addOrg(this._dbManager, userId, req.body);
|
||||
return sendOkReply(req, res, orgId);
|
||||
const org = await addOrg(this._dbManager, userId, req.body);
|
||||
this._logCreateSiteEvents(req, org);
|
||||
return sendOkReply(req, res, org.id);
|
||||
}));
|
||||
|
||||
// PATCH /api/orgs/:oid
|
||||
@@ -176,32 +179,30 @@ export class ApiServer {
|
||||
// Update the specified org.
|
||||
this._app.patch('/api/orgs/:oid', expressWrap(async (req, res) => {
|
||||
const org = getOrgKey(req);
|
||||
const query = await this._dbManager.updateOrg(getScope(req), org, req.body);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.updateOrg(getScope(req), org, req.body);
|
||||
if (data && (req.body.name || req.body.domain)) {
|
||||
this._logRenameSiteEvents(req as RequestWithLogin, data);
|
||||
}
|
||||
return sendReply(req, res, result);
|
||||
}));
|
||||
|
||||
// DELETE /api/orgs/:oid
|
||||
// Delete the specified org and all included workspaces and docs.
|
||||
this._app.delete('/api/orgs/:oid', expressWrap(async (req, res) => {
|
||||
const org = getOrgKey(req);
|
||||
const query = await this._dbManager.deleteOrg(getScope(req), org);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.deleteOrg(getScope(req), org);
|
||||
if (data) { this._logDeleteSiteEvents(req, data); }
|
||||
return sendReply(req, res, {...result, data: data?.id});
|
||||
}));
|
||||
|
||||
// POST /api/orgs/:oid/workspaces
|
||||
// Body params: name
|
||||
// Create a new workspace owned by the specific organization.
|
||||
this._app.post('/api/orgs/:oid/workspaces', expressWrap(async (req, res) => {
|
||||
const mreq = req as RequestWithLogin;
|
||||
const org = getOrgKey(req);
|
||||
const query = await this._dbManager.addWorkspace(getScope(req), org, req.body);
|
||||
this._gristServer.getTelemetry().logEvent(mreq, 'createdWorkspace', {
|
||||
full: {
|
||||
workspaceId: query.data,
|
||||
userId: mreq.userId,
|
||||
},
|
||||
});
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.addWorkspace(getScope(req), org, req.body);
|
||||
if (data) { this._logCreateWorkspaceEvents(req, data); }
|
||||
return sendReply(req, res, {...result, data: data?.id});
|
||||
}));
|
||||
|
||||
// PATCH /api/workspaces/:wid
|
||||
@@ -209,23 +210,18 @@ export class ApiServer {
|
||||
// Update the specified workspace.
|
||||
this._app.patch('/api/workspaces/:wid', expressWrap(async (req, res) => {
|
||||
const wsId = integerParam(req.params.wid, 'wid');
|
||||
const query = await this._dbManager.updateWorkspace(getScope(req), wsId, req.body);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.updateWorkspace(getScope(req), wsId, req.body);
|
||||
if (data && 'name' in req.body) { this._logRenameWorkspaceEvents(req, data); }
|
||||
return sendReply(req, res, {...result, data: data?.current.id});
|
||||
}));
|
||||
|
||||
// DELETE /api/workspaces/:wid
|
||||
// Delete the specified workspace and all included docs.
|
||||
this._app.delete('/api/workspaces/:wid', expressWrap(async (req, res) => {
|
||||
const mreq = req as RequestWithLogin;
|
||||
const wsId = integerParam(req.params.wid, 'wid');
|
||||
const query = await this._dbManager.deleteWorkspace(getScope(req), wsId);
|
||||
this._gristServer.getTelemetry().logEvent(mreq, 'deletedWorkspace', {
|
||||
full: {
|
||||
workspaceId: wsId,
|
||||
userId: mreq.userId,
|
||||
},
|
||||
});
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.deleteWorkspace(getScope(req), wsId);
|
||||
if (data) { this._logDeleteWorkspaceEvents(req, data); }
|
||||
return sendReply(req, res, {...result, data: data?.id});
|
||||
}));
|
||||
|
||||
// POST /api/workspaces/:wid/remove
|
||||
@@ -234,17 +230,12 @@ export class ApiServer {
|
||||
this._app.post('/api/workspaces/:wid/remove', expressWrap(async (req, res) => {
|
||||
const wsId = integerParam(req.params.wid, 'wid');
|
||||
if (isParameterOn(req.query.permanent)) {
|
||||
const mreq = req as RequestWithLogin;
|
||||
const query = await this._dbManager.deleteWorkspace(getScope(req), wsId);
|
||||
this._gristServer.getTelemetry().logEvent(mreq, 'deletedWorkspace', {
|
||||
full: {
|
||||
workspaceId: query.data,
|
||||
userId: mreq.userId,
|
||||
},
|
||||
});
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.deleteWorkspace(getScope(req), wsId);
|
||||
if (data) { this._logDeleteWorkspaceEvents(req, data); }
|
||||
return sendReply(req, res, {...result, data: data?.id});
|
||||
} 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);
|
||||
}
|
||||
}));
|
||||
@@ -254,7 +245,8 @@ export class ApiServer {
|
||||
// still available.
|
||||
this._app.post('/api/workspaces/:wid/unremove', expressWrap(async (req, res) => {
|
||||
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);
|
||||
}));
|
||||
|
||||
@@ -262,9 +254,9 @@ export class ApiServer {
|
||||
// Create a new doc owned by the specific workspace.
|
||||
this._app.post('/api/workspaces/:wid/docs', expressWrap(async (req, res) => {
|
||||
const wsId = integerParam(req.params.wid, 'wid');
|
||||
const result = await this._dbManager.addDocument(getScope(req), wsId, req.body);
|
||||
if (result.status === 200) { this._logCreateDocumentEvents(req, result.data!); }
|
||||
return sendReply(req, res, {...result, data: result.data?.id});
|
||||
const {data, ...result} = await this._dbManager.addDocument(getScope(req), wsId, req.body);
|
||||
if (data) { this._logCreateDocumentEvents(req, data); }
|
||||
return sendReply(req, res, {...result, data: data?.id});
|
||||
}));
|
||||
|
||||
// GET /api/templates/
|
||||
@@ -301,16 +293,17 @@ export class ApiServer {
|
||||
// PATCH /api/docs/:did
|
||||
// Update the specified doc.
|
||||
this._app.patch('/api/docs/:did', expressWrap(async (req, res) => {
|
||||
const query = await this._dbManager.updateDocument(getDocScope(req), req.body);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.updateDocument(getDocScope(req), req.body);
|
||||
if (data && 'name' in req.body) { this._logRenameDocumentEvents(req, data); }
|
||||
return sendReply(req, res, {...result, data: data?.current.id});
|
||||
}));
|
||||
|
||||
// POST /api/docs/:did/unremove
|
||||
// Recover the specified doc if it was previously soft-deleted and is
|
||||
// still available.
|
||||
this._app.post('/api/docs/:did/unremove', expressWrap(async (req, res) => {
|
||||
const {status, data} = await this._dbManager.undeleteDocument(getDocScope(req));
|
||||
if (status === 200) { this._logRestoreDocumentEvents(req, data!); }
|
||||
const {data} = await this._dbManager.undeleteDocument(getDocScope(req));
|
||||
if (data) { this._logRestoreDocumentEvents(req, data); }
|
||||
return sendOkReply(req, res);
|
||||
}));
|
||||
|
||||
@@ -319,8 +312,9 @@ export class ApiServer {
|
||||
this._app.patch('/api/orgs/:oid/access', expressWrap(async (req, res) => {
|
||||
const org = getOrgKey(req);
|
||||
const delta = req.body.delta;
|
||||
const query = await this._dbManager.updateOrgPermissions(getScope(req), org, delta);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.updateOrgPermissions(getScope(req), org, delta);
|
||||
if (data) { this._logChangeSiteAccessEvents(req as RequestWithLogin, data); }
|
||||
return sendReply(req, res, result);
|
||||
}));
|
||||
|
||||
// PATCH /api/workspaces/:wid/access
|
||||
@@ -328,8 +322,9 @@ export class ApiServer {
|
||||
this._app.patch('/api/workspaces/:wid/access', expressWrap(async (req, res) => {
|
||||
const workspaceId = integerParam(req.params.wid, 'wid');
|
||||
const delta = req.body.delta;
|
||||
const query = await this._dbManager.updateWorkspacePermissions(getScope(req), workspaceId, delta);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.updateWorkspacePermissions(getScope(req), workspaceId, delta);
|
||||
if (data) { this._logChangeWorkspaceAccessEvents(req as RequestWithLogin, data); }
|
||||
return sendReply(req, res, result);
|
||||
}));
|
||||
|
||||
// GET /api/docs/:did
|
||||
@@ -343,28 +338,30 @@ export class ApiServer {
|
||||
// Update the specified doc acl rules.
|
||||
this._app.patch('/api/docs/:did/access', expressWrap(async (req, res) => {
|
||||
const delta = req.body.delta;
|
||||
const query = await this._dbManager.updateDocPermissions(getDocScope(req), delta);
|
||||
this._logInvitedDocUserTelemetryEvents(req as RequestWithLogin, delta);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.updateDocPermissions(getDocScope(req), delta);
|
||||
if (data) { this._logChangeDocumentAccessEvents(req as RequestWithLogin, data); }
|
||||
return sendReply(req, res, result);
|
||||
}));
|
||||
|
||||
// PATCH /api/docs/:did/move
|
||||
// Move the doc to the workspace specified in the body.
|
||||
this._app.patch('/api/docs/:did/move', expressWrap(async (req, res) => {
|
||||
const workspaceId = integerParam(req.body.workspace, 'workspace');
|
||||
const result = await this._dbManager.moveDoc(getDocScope(req), workspaceId);
|
||||
if (result.status === 200) { this._logMoveDocumentEvents(req, result.data!); }
|
||||
return sendReply(req, res, {...result, data: result.data?.current.id});
|
||||
const {data, ...result} = await this._dbManager.moveDoc(getDocScope(req), workspaceId);
|
||||
if (data) { this._logMoveDocumentEvents(req, data); }
|
||||
return sendReply(req, res, {...result, data: data?.current.id});
|
||||
}));
|
||||
|
||||
this._app.patch('/api/docs/:did/pin', expressWrap(async (req, res) => {
|
||||
const query = await this._dbManager.pinDoc(getDocScope(req), true);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.pinDoc(getDocScope(req), true);
|
||||
if (data) { this._logPinDocumentEvents(req, data); }
|
||||
return sendReply(req, res, result);
|
||||
}));
|
||||
|
||||
this._app.patch('/api/docs/:did/unpin', expressWrap(async (req, res) => {
|
||||
const query = await this._dbManager.pinDoc(getDocScope(req), false);
|
||||
return sendReply(req, res, query);
|
||||
const {data, ...result} = await this._dbManager.pinDoc(getDocScope(req), false);
|
||||
if (data) { this._logUnpinDocumentEvents(req, data); }
|
||||
return sendReply(req, res, result);
|
||||
}));
|
||||
|
||||
// GET /api/orgs/:oid/access
|
||||
@@ -408,7 +405,8 @@ export class ApiServer {
|
||||
throw new ApiError('Name expected in the body', 400);
|
||||
}
|
||||
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);
|
||||
}));
|
||||
|
||||
@@ -489,6 +487,7 @@ export class ApiServer {
|
||||
if (!user) { return handleDeletedUser(); }
|
||||
if (!user.apiKey || force) {
|
||||
user = await updateApiKeyWithRetry(manager, user);
|
||||
this._logCreateUserAPIKeyEvents(req);
|
||||
res.status(200).send(user.apiKey);
|
||||
} else {
|
||||
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(); }
|
||||
user.apiKey = null;
|
||||
await manager.save(User, user);
|
||||
this._logDeleteUserAPIKeyEvents(req);
|
||||
});
|
||||
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) {
|
||||
const {workspace} = document;
|
||||
const {id, name, workspace} = document;
|
||||
this._gristServer.getAuditLogger().logEvent(req as RequestWithLogin, {
|
||||
event: {
|
||||
name: 'restoreDocumentFromTrash',
|
||||
details: {
|
||||
document: {
|
||||
id: document.id,
|
||||
name: document.name,
|
||||
},
|
||||
id,
|
||||
name,
|
||||
workspace: {
|
||||
id: workspace.id,
|
||||
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) {
|
||||
if (!delta.users) { return; }
|
||||
|
||||
@@ -722,17 +758,13 @@ export class ApiServer {
|
||||
name: 'moveDocument',
|
||||
details: {
|
||||
id: current.id,
|
||||
previous: {
|
||||
workspace: {
|
||||
id: previous.workspace.id,
|
||||
name: previous.workspace.name,
|
||||
},
|
||||
previousWorkspace: {
|
||||
id: previous.workspace.id,
|
||||
name: previous.workspace.name,
|
||||
},
|
||||
current: {
|
||||
workspace: {
|
||||
id: current.workspace.id,
|
||||
name: current.workspace.name,
|
||||
},
|
||||
newWorkspace: {
|
||||
id: current.workspace.id,
|
||||
name: current.workspace.name,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
@@ -741,6 +773,192 @@ export class ApiServer {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public async updateUser(userId: number, props: UserProfileChange) {
|
||||
const { user, isWelcomed } = await this._usersManager.updateUser(userId, props);
|
||||
if (user && isWelcomed) {
|
||||
this.emit('firstLogin', this.makeFullUser(user));
|
||||
public async updateUser(
|
||||
userId: number,
|
||||
props: UserProfileChange
|
||||
): 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>) {
|
||||
@@ -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 name: desired org name
|
||||
@@ -1073,12 +1077,17 @@ export class HomeDBManager extends EventEmitter {
|
||||
* meaningful for team sites currently.
|
||||
* @param billing: if set, controls the billing account settings for the org.
|
||||
*/
|
||||
public async addOrg(user: User, props: Partial<OrganizationProperties>,
|
||||
options: { setUserAsOwner: boolean,
|
||||
useNewPlan: boolean,
|
||||
product?: string, // Default to PERSONAL_FREE_PLAN or TEAM_FREE_PLAN env variable.
|
||||
billing?: BillingOptions},
|
||||
transaction?: EntityManager): Promise<QueryResult<number>> {
|
||||
public async addOrg(
|
||||
user: User,
|
||||
props: Partial<OrganizationProperties>,
|
||||
options: {
|
||||
setUserAsOwner: boolean,
|
||||
useNewPlan: boolean,
|
||||
product?: string, // Default to PERSONAL_FREE_PLAN or TEAM_FREE_PLAN env variable.
|
||||
billing?: BillingOptions
|
||||
},
|
||||
transaction?: EntityManager
|
||||
): Promise<QueryResult<Organization>> {
|
||||
const notifications: Array<() => void> = [];
|
||||
const name = props.name;
|
||||
const domain = props.domain;
|
||||
@@ -1219,10 +1228,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
// Emit a notification.
|
||||
notifications.push(this._teamCreatorNotification(user.id));
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
data: savedOrg.id
|
||||
};
|
||||
return {status: 200, data: savedOrg};
|
||||
});
|
||||
for (const notification of notifications) { notification(); }
|
||||
return orgResult;
|
||||
@@ -1230,8 +1236,8 @@ export class HomeDBManager extends EventEmitter {
|
||||
|
||||
// If setting anything more than prefs:
|
||||
// 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
|
||||
// query result with status 200 on success.
|
||||
// error. Otherwise updates the given org with the given name. Returns a query
|
||||
// result with status 200 on success.
|
||||
// For setting userPrefs or userOrgPrefs:
|
||||
// These are user-specific setting, so are allowed with VIEW access (that includes
|
||||
// guests). Prefs are replaced in their entirety, not merged.
|
||||
@@ -1242,7 +1248,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
orgKey: string|number,
|
||||
props: Partial<OrganizationProperties>,
|
||||
transaction?: EntityManager,
|
||||
): Promise<QueryResult<number>> {
|
||||
): Promise<QueryResult<PreviousAndCurrent<Organization>>> {
|
||||
|
||||
// Check the scope of the modifications.
|
||||
let markPermissions: number = Permissions.VIEW;
|
||||
@@ -1272,11 +1278,12 @@ export class HomeDBManager extends EventEmitter {
|
||||
});
|
||||
const queryResult = await verifyEntity(orgQuery);
|
||||
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;
|
||||
}
|
||||
// Update the fields and save.
|
||||
const org: Organization = queryResult.data;
|
||||
const previous = structuredClone(org);
|
||||
org.checkProperties(props);
|
||||
if (modifyOrg) {
|
||||
if (props.domain) {
|
||||
@@ -1312,15 +1319,18 @@ export class HomeDBManager extends EventEmitter {
|
||||
.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
|
||||
// error. Otherwise deletes the given org. Returns an empty query result with
|
||||
// status 200 on success.
|
||||
public async deleteOrg(scope: Scope, orgKey: string|number,
|
||||
transaction?: EntityManager): Promise<QueryResult<number>> {
|
||||
// error. Otherwise deletes the given org. Returns a query result with status 200
|
||||
// on success.
|
||||
public async deleteOrg(
|
||||
scope: Scope,
|
||||
orgKey: string|number,
|
||||
transaction?: EntityManager
|
||||
): Promise<QueryResult<Organization>> {
|
||||
return await this._runInTransaction(transaction, async manager => {
|
||||
const orgQuery = this.org(scope, orgKey, {
|
||||
manager,
|
||||
@@ -1344,6 +1354,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
return queryResult;
|
||||
}
|
||||
const org: Organization = queryResult.data;
|
||||
const deletedOrg = structuredClone(org);
|
||||
// Delete the org, org ACLs/groups, workspaces, workspace ACLs/groups, workspace docs
|
||||
// and doc ACLs/groups.
|
||||
const orgGroups = org.aclRules.map(orgAcl => orgAcl.group);
|
||||
@@ -1363,15 +1374,18 @@ export class HomeDBManager extends EventEmitter {
|
||||
if (billingAccount && billingAccount.orgs.length === 0) {
|
||||
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.
|
||||
// Otherwise adds a workspace with the given name. Returns a query result with the id
|
||||
// of the added workspace.
|
||||
public async addWorkspace(scope: Scope, orgKey: string|number,
|
||||
props: Partial<WorkspaceProperties>): Promise<QueryResult<number>> {
|
||||
// Otherwise adds a workspace with the given name. Returns a query result with the
|
||||
// added workspace.
|
||||
public async addWorkspace(
|
||||
scope: Scope,
|
||||
orgKey: string|number,
|
||||
props: Partial<WorkspaceProperties>
|
||||
): Promise<QueryResult<Workspace>> {
|
||||
const name = props.name;
|
||||
if (!name) {
|
||||
return {
|
||||
@@ -1414,18 +1428,18 @@ export class HomeDBManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
const workspace = await this._doAddWorkspace({org, props, ownerId: scope.userId}, manager);
|
||||
return {
|
||||
status: 200,
|
||||
data: workspace.id
|
||||
};
|
||||
return {status: 200, data: workspace};
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
// query result with status 200 on success.
|
||||
public async updateWorkspace(scope: Scope, wsId: number,
|
||||
props: Partial<WorkspaceProperties>): Promise<QueryResult<number>> {
|
||||
// error. Otherwise updates the given workspace with the given name. Returns a query result
|
||||
// with status 200 on success.
|
||||
public async updateWorkspace(
|
||||
scope: Scope,
|
||||
wsId: number,
|
||||
props: Partial<WorkspaceProperties>
|
||||
): Promise<QueryResult<PreviousAndCurrent<Workspace>>> {
|
||||
return await this._connection.transaction(async manager => {
|
||||
const wsQuery = this._workspace(scope, wsId, {
|
||||
manager,
|
||||
@@ -1438,17 +1452,18 @@ export class HomeDBManager extends EventEmitter {
|
||||
}
|
||||
// Update the name and save.
|
||||
const workspace: Workspace = queryResult.data;
|
||||
const previous = structuredClone(workspace);
|
||||
workspace.checkProperties(props);
|
||||
workspace.updateFromProperties(props);
|
||||
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
|
||||
// error. Otherwise deletes the given workspace. Returns an empty query result with
|
||||
// status 200 on success.
|
||||
public async deleteWorkspace(scope: Scope, wsId: number): Promise<QueryResult<number>> {
|
||||
// error. Otherwise deletes the given workspace. Returns a query result with status 200
|
||||
// on success.
|
||||
public async deleteWorkspace(scope: Scope, wsId: number): Promise<QueryResult<Workspace>> {
|
||||
return await this._connection.transaction(async manager => {
|
||||
const wsQuery = this._workspace(scope, wsId, {
|
||||
manager,
|
||||
@@ -1469,6 +1484,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
return queryResult;
|
||||
}
|
||||
const workspace: Workspace = queryResult.data;
|
||||
const deletedWorkspace = structuredClone(workspace);
|
||||
// Delete the workspace, workspace docs, doc ACLs/groups and workspace ACLs/groups.
|
||||
const wsGroups = workspace.aclRules.map(wsAcl => wsAcl.group);
|
||||
const docAcls = ([] as AclRule[]).concat(...workspace.docs.map(doc => doc.aclRules));
|
||||
@@ -1477,15 +1493,15 @@ export class HomeDBManager extends EventEmitter {
|
||||
...workspace.aclRules, ...docGroups]);
|
||||
// Update the guests in the org after removing this workspace.
|
||||
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());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// error. Otherwise updates the given doc with the given name. Returns an empty
|
||||
// query result with status 200 on success.
|
||||
// error. Otherwise updates the given doc with the given name. Returns a query result with
|
||||
// status 200 on success.
|
||||
// NOTE: This does not update the updateAt date indicating the last modified time of the doc.
|
||||
// We may want to make it do so.
|
||||
public async updateDocument(
|
||||
scope: DocScope,
|
||||
props: Partial<DocumentProperties>,
|
||||
transaction?: EntityManager
|
||||
): Promise<QueryResult<number>> {
|
||||
): Promise<QueryResult<PreviousAndCurrent<Document>>> {
|
||||
const markPermissions = Permissions.SCHEMA_EDIT;
|
||||
return await this._runInTransaction(transaction, async (manager) => {
|
||||
const {forkId} = parseUrlId(scope.urlId);
|
||||
@@ -1721,6 +1737,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
}
|
||||
// Update the name and save.
|
||||
const doc: Document = queryResult.data;
|
||||
const previous = structuredClone(doc);
|
||||
doc.checkProperties(props);
|
||||
doc.updateFromProperties(props);
|
||||
if (forkId) {
|
||||
@@ -1752,7 +1769,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
.execute();
|
||||
// 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,
|
||||
orgKey: string|number,
|
||||
delta: PermissionDelta
|
||||
): Promise<QueryResult<void>> {
|
||||
): Promise<QueryResult<PermissionDelta & {organization: Organization}>> {
|
||||
const {userId} = scope;
|
||||
const notifications: Array<() => void> = [];
|
||||
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.
|
||||
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(); }
|
||||
return result;
|
||||
@@ -1966,7 +1986,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
scope: Scope,
|
||||
wsId: number,
|
||||
delta: PermissionDelta
|
||||
): Promise<QueryResult<void>> {
|
||||
): Promise<QueryResult<PermissionDelta & {workspace: Workspace}>> {
|
||||
const {userId} = scope;
|
||||
const notifications: Array<() => void> = [];
|
||||
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);
|
||||
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(); }
|
||||
return result;
|
||||
@@ -2041,7 +2068,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
public async updateDocPermissions(
|
||||
scope: DocScope,
|
||||
delta: PermissionDelta
|
||||
): Promise<QueryResult<void>> {
|
||||
): Promise<QueryResult<PermissionDelta & {document: Document}>> {
|
||||
const notifications: Array<() => void> = [];
|
||||
const result = await this._connection.transaction(async manager => {
|
||||
const {userId} = scope;
|
||||
@@ -2082,7 +2109,14 @@ export class HomeDBManager extends EventEmitter {
|
||||
await this._repairOrgGuests(scope, doc.workspace.org.id, manager);
|
||||
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(); }
|
||||
return result;
|
||||
@@ -2386,7 +2420,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
public async pinDoc(
|
||||
scope: DocScope,
|
||||
setPinned: boolean
|
||||
): Promise<QueryResult<void>> {
|
||||
): Promise<QueryResult<Document>> {
|
||||
return await this._connection.transaction(async manager => {
|
||||
// Find the doc to assert that it exists. Assert that the user has edit access to the
|
||||
// parent org.
|
||||
@@ -2410,7 +2444,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
// Save and return success status.
|
||||
await manager.save(doc);
|
||||
}
|
||||
return { status: 200 };
|
||||
return {status: 200, data: doc};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4291,9 +4325,9 @@ export class HomeDBManager extends EventEmitter {
|
||||
markPermissions: Permissions.REMOVE
|
||||
});
|
||||
const workspace: Workspace = this.unwrapQueryResult(await verifyEntity(wsQuery));
|
||||
await manager.createQueryBuilder()
|
||||
.update(Workspace).set({removedAt}).where({id: workspace.id})
|
||||
.execute();
|
||||
workspace.removedAt = removedAt;
|
||||
const data = await manager.save(workspace);
|
||||
return {status: 200, data};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -256,14 +256,17 @@ export class UsersManager {
|
||||
});
|
||||
}
|
||||
|
||||
public async updateUser(userId: number, props: UserProfileChange) {
|
||||
let isWelcomed: boolean = false;
|
||||
let user: User|null = null;
|
||||
await this._connection.transaction(async manager => {
|
||||
user = await manager.findOne(User, {relations: ['logins'],
|
||||
where: {id: userId}});
|
||||
public async updateUser(userId: number, props: UserProfileChange){
|
||||
return await this._connection.transaction(async manager => {
|
||||
let isWelcomed = 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); }
|
||||
|
||||
const previous = structuredClone(user);
|
||||
if (props.name && props.name !== user.name) {
|
||||
user.name = props.name;
|
||||
needsSave = true;
|
||||
@@ -279,8 +282,8 @@ export class UsersManager {
|
||||
if (needsSave) {
|
||||
await manager.save(user);
|
||||
}
|
||||
return {previous, current: user, isWelcomed};
|
||||
});
|
||||
return { user, isWelcomed };
|
||||
}
|
||||
|
||||
// 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.
|
||||
const userOrgPrefs: UserOrgPrefs = {showGristTour: true};
|
||||
const orgId = result.data;
|
||||
if (orgId) {
|
||||
await this._homeDb.updateOrg({userId: user.id}, orgId, {userOrgPrefs}, manager);
|
||||
const org = result.data;
|
||||
if (org) {
|
||||
await this._homeDb.updateOrg({userId: user.id}, org.id, {userOrgPrefs}, manager);
|
||||
}
|
||||
}
|
||||
if (needUpdate) {
|
||||
|
||||
Reference in New Issue
Block a user