Skip to content

Commit

Permalink
Implemented Workspace Authorization & Member Management
Browse files Browse the repository at this point in the history
  • Loading branch information
R1c4rdCo5t4 committed Jun 19, 2024
1 parent 0e8ab34 commit 3ec61a9
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 32 deletions.
10 changes: 4 additions & 6 deletions code/server/src/controllers/http/handlers/resourcesHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Request, Response } from 'express';
import { ResourcesService } from '@services/ResourcesService';
import { InvalidParameterError } from '@domain/errors/errors';
import { Server } from 'socket.io';
import { verifyToken } from '@controllers/http/middlewares/authMiddleware';

function resourcesHandlers(service: ResourcesService, io: Server) {
/**
Expand All @@ -15,15 +16,13 @@ function resourcesHandlers(service: ResourcesService, io: Server) {
const createResource = async (req: Request, res: Response) => {
const { wid } = req.params;
if (!wid) throw new InvalidParameterError('Workspace id is required');

const resource = req.body as ResourceInputModel;
if (!resource) throw new InvalidParameterError('Body is required');
const { type, name, parent } = resource;
if (!type) throw new InvalidParameterError('Resource type is required');

const id = await service.createResource(wid, name, type, parent);
const now = new Date().toISOString();

const createdResource: Resource = {
id,
workspace: wid,
Expand Down Expand Up @@ -61,7 +60,6 @@ function resourcesHandlers(service: ResourcesService, io: Server) {
const { wid, id } = req.params;
if (!wid) throw new InvalidParameterError('Workspace id is required');
if (!id) throw new InvalidParameterError('Resource id is required');

const resource = req.body as Partial<Resource>;
if (!resource) throw new InvalidParameterError('Body is required');

Expand All @@ -86,10 +84,10 @@ function resourcesHandlers(service: ResourcesService, io: Server) {
};

const router = PromiseRouter({ mergeParams: true });
router.post('/', createResource);
router.post('/', verifyToken, createResource);
router.put('/:id', verifyToken, updateResource);
router.delete('/:id', verifyToken, deleteResource);
router.get('/:id', getResource);
router.put('/:id', updateResource);
router.delete('/:id', deleteResource);

return router;
}
Expand Down
7 changes: 4 additions & 3 deletions code/server/src/controllers/http/handlers/usersHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Request, Response } from 'express';
import { UsersService } from '@services/UsersService';
import { httpResponse } from '@controllers/http/utils/httpResponse';
import { UserData } from '@notespace/shared/src/users/types';
import { verifyToken } from '@controllers/http/middlewares/authMiddleware';

function usersHandlers(service: UsersService) {
const registerUser = async (req: Request, res: Response) => {
Expand Down Expand Up @@ -46,10 +47,10 @@ function usersHandlers(service: UsersService) {
};

const router = PromiseRouter({ mergeParams: true });
router.post('/', registerUser);
router.post('/', verifyToken, registerUser);
router.put('/:id', verifyToken, updateUser);
router.delete('/:id', verifyToken, deleteUser);
router.get('/:id', getUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
router.get('/', getUsers);

return router;
Expand Down
34 changes: 24 additions & 10 deletions code/server/src/controllers/http/handlers/workspacesHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import PromiseRouter from 'express-promise-router';
import resourcesHandlers from '@controllers/http/handlers/resourcesHandlers';
import { httpResponse } from '@controllers/http/utils/httpResponse';
import { Request, Response } from 'express';
import { NextFunction, Request, Response } from 'express';
import { WorkspaceInputModel, WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
import { Services } from '@services/Services';
import { Server } from 'socket.io';
import { InvalidParameterError } from '@domain/errors/errors';
import { ForbiddenError, InvalidParameterError } from '@domain/errors/errors';
import { verifyToken } from '@controllers/http/middlewares/authMiddleware';

function workspacesHandlers(services: Services, io: Server) {
const createWorkspace = async (req: Request, res: Response) => {
const { name, isPrivate } = req.body as WorkspaceInputModel;
if (!name) throw new InvalidParameterError('Workspace name is required');
if (isPrivate === undefined) throw new InvalidParameterError('Workspace visibility is required');

const member = req.user!.email;
const id = await services.workspaces.createWorkspace(name, isPrivate);
const member = { email: req.user!.email, name: req.user!.name };
await services.workspaces.addWorkspaceMember(id, member);
const workspace: WorkspaceMeta = {
id,
name,
Expand Down Expand Up @@ -83,17 +85,29 @@ function workspacesHandlers(services: Services, io: Server) {
httpResponse.noContent(res).send();
};

async function canAccessWorkspace(req: Request, res: Response, next: NextFunction) {
// is a workspace member or workspace is public
const { wid } = req.params;
if (!wid) throw new InvalidParameterError('Workspace id is required');
const workspace = await services.workspaces.getWorkspace(wid);
if (!workspace) throw new InvalidParameterError('Workspace not found');
if (workspace.isPrivate && !workspace.members.find(m => m === req.user?.email)) {
throw new ForbiddenError('User is not a member of this workspace');
}
next();
}

const router = PromiseRouter();
router.post('/', createWorkspace);
router.post('/', verifyToken, createWorkspace);
router.get('/', getWorkspaces);
router.get('/:wid', getWorkspace);
router.put('/:wid', updateWorkspace);
router.delete('/:wid', deleteWorkspace);
router.post('/:wid/members', addMemberToWorkspace);
router.delete('/:wid/members', removeMemberFromWorkspace);
router.get('/:wid', canAccessWorkspace, getWorkspace);
router.put('/:wid', verifyToken, canAccessWorkspace, updateWorkspace);
router.delete('/:wid', verifyToken, canAccessWorkspace, deleteWorkspace);
router.post('/:wid/members', verifyToken, canAccessWorkspace, addMemberToWorkspace);
router.delete('/:wid/members', verifyToken, canAccessWorkspace, removeMemberFromWorkspace);

// sub-routes for resources (documents and folders)
router.use('/:wid', resourcesHandlers(services.resources, io));
router.use('/:wid', canAccessWorkspace, resourcesHandlers(services.resources, io));
return router;
}

Expand Down
5 changes: 2 additions & 3 deletions code/server/src/controllers/http/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import workspacesHandlers from '@controllers/http/handlers/workspacesHandlers';
import errorMiddleware from '@controllers/http/middlewares/errorMiddleware';
import usersHandlers from '@controllers/http/handlers/usersHandlers';
import { Server } from 'socket.io';
import { verifyToken } from '@controllers/http/middlewares/authMiddleware';

export default function (services: Services, io: Server) {
if (!services) throw new Error('Services parameter is required');
Expand All @@ -14,8 +13,8 @@ export default function (services: Services, io: Server) {
const router = PromiseRouter();
router.use(express.urlencoded({ extended: true }));

router.use('/users', verifyToken, usersHandlers(services.users));
router.use('/workspaces', verifyToken, workspacesHandlers(services, io));
router.use('/users', usersHandlers(services.users));
router.use('/workspaces', workspacesHandlers(services, io));
router.use(errorMiddleware);
return router;
}
14 changes: 13 additions & 1 deletion code/server/src/databases/memory/MemoryWorkspacesDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class MemoryWorkspacesDB implements WorkspacesRepository {
updatedAt: '',
};
const now = new Date().toISOString();
Memory.workspaces[id] = { id, name, isPrivate, resources: { [id]: root }, createdAt: now, members: [''] };
Memory.workspaces[id] = { id, name, isPrivate, resources: { [id]: root }, createdAt: now, members: [] };
return id;
}

Expand Down Expand Up @@ -59,4 +59,16 @@ export class MemoryWorkspacesDB implements WorkspacesRepository {

delete Memory.workspaces[id];
}

async addWorkspaceMember(wid: string, email: string): Promise<void> {
const workspace = Memory.workspaces[wid];
if (!workspace) throw new NotFoundError(`Workspace not found`);
Memory.workspaces[wid].members.push(email);
}

async removeWorkspaceMember(wid: string, email: string): Promise<void> {
const workspace = Memory.workspaces[wid];
if (!workspace) throw new NotFoundError(`Workspace not found`);
Memory.workspaces[wid].members = Memory.workspaces[wid].members.filter(member => member !== email);
}
}
34 changes: 27 additions & 7 deletions code/server/src/databases/postgres/PostgresWorkspacesDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ export class PostgresWorkspacesDB implements WorkspacesRepository {
return (
await sql`
select row_to_json(t) as resources
from(
select id, name, type, parent, children
from resource
where workspace = ${wid}
group by id
order by created_at desc
) as t
from (
select id, name, type, parent, children
from resource
where workspace = ${wid}
group by id
order by created_at desc
) as t
`
).map(r => r.resources);
}
Expand All @@ -58,4 +58,24 @@ export class PostgresWorkspacesDB implements WorkspacesRepository {
`;
if (isEmpty(results)) throw new NotFoundError(`Workspace not found`);
}

async addWorkspaceMember(wid: string, email: string): Promise<void> {
const results = await sql`
update workspace
set members = array_append(members, ${email}::char(16))
where id = ${wid} and not ${email} = any(members)
returning id
`;
if (isEmpty(results)) throw new NotFoundError(`Workspace not found or member already in workspace`);
}

async removeWorkspaceMember(wid: string, email: string): Promise<void> {
const results = await sql`
update workspace
set members = array_remove(members, ${email}::char(16))
where id = ${wid}
returning id
`;
if (isEmpty(results)) throw new NotFoundError(`Workspace not found or member does not exist`);
}
}
3 changes: 1 addition & 2 deletions code/shared/src/workspace/types/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Resource } from "./resource";
import { UserData } from "../../users/types";

export type WorkspaceMeta = {
name: string;
id: string;
createdAt: string;
members: UserData[];
members: string[];
isPrivate: boolean;
};

Expand Down

0 comments on commit 3ec61a9

Please sign in to comment.