Skip to content

Commit

Permalink
feat: logos (#183)
Browse files Browse the repository at this point in the history
  • Loading branch information
gempain authored Jan 12, 2021
1 parent e215f0e commit 0074e42
Show file tree
Hide file tree
Showing 33 changed files with 807 additions and 5 deletions.
8 changes: 8 additions & 0 deletions src/db/setup-db-indexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ import { Members } from '../entities/members/member';
import { ApiTokens } from '../entities/api/api-token';
import { HookDeliveries } from '../hooks/hook-delivery';
import { Hooks } from '../hooks/hook';
import { Orgs } from '../entities/orgs/org';

export async function setupDbIndexes() {
await configureIndexes(AppDb.client, {
[Orgs().collectionName]: [
{
fieldOrSpec: {
'invites.token': 1,
},
},
],
[Users().collectionName]: [
{
fieldOrSpec: {
Expand Down
9 changes: 9 additions & 0 deletions src/entities/api/api-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export enum ApiScope {
org_read = 'org.read',
org_list = 'org.list',
org_update = 'org.update',
org_logo_get = 'org.logo.get',
org_logo_set = 'org.logo.set',
org_logo_remove = 'org.logo.remove',
org_hook_list = 'org.hook.list',
org_hook_create = 'org.hook.create',
org_hook_read = 'org.hook.read',
Expand All @@ -30,6 +33,9 @@ export enum ApiScope {
team_read = 'team.read',
team_update = 'team.update',
team_delete = 'team.delete',
team_logo_set = 'team.logo.set',
team_logo_remove = 'team.logo.remove',
team_logo_get = 'team.logo.get',
team_member_list = 'team.member.list',
team_member_add = 'team.member.add',
team_member_delete = 'team.member.delete',
Expand All @@ -44,6 +50,9 @@ export enum ApiScope {
site_read = 'site.read',
site_update = 'site.update',
site_delete = 'site.delete',
site_logo_set = 'site.logo.set',
site_logo_remove = 'site.logo.remove',
site_logo_get = 'site.logo.get',
site_password_set = 'site.password.set',
site_password_remove = 'site.password.remove',
site_name_validate = 'site.name.validate',
Expand Down
1 change: 1 addition & 0 deletions src/entities/invites/serialize-invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function serializeUserInvite(org: Org, invite: Invite) {
org: {
name: org.name,
color: org.color,
logo: org.logo,
},
expiresAt: invite.expiresAt,
memberOptions: invite.memberOptions,
Expand Down
40 changes: 40 additions & 0 deletions src/entities/orgs/guards/can-get-org-logo-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { params } from '../../../commons/express-joi/params';
import { object } from 'joi';
import { NextFunction, Request, Response } from 'express';
import { $id } from '../../../utils/id';
import { Orgs } from '../org';
import { isAdminOrOwner } from '../../../auth/guards/is-admin-or-owner';
import { getUser } from '../../../auth/utils/get-user';
import { wrapAsyncMiddleware } from '../../../commons/utils/wrap-async-middleware';
import { ForbiddenError } from '../../../commons/errors/forbidden-error';

export const canGetOrgLogoGuard = [
params(object({
orgId: $id,
})),
wrapAsyncMiddleware(async (req: Request, res: Response, next: NextFunction) => {
const { orgId } = req.params;

// check is admin or owner
const user = getUser(req);
const adminOrOwner = await isAdminOrOwner(user._id, orgId);
if (adminOrOwner) {
return next();
}

// check if user has an invite token
if (req.query.invite) {
const count = await Orgs().countDocuments({
'invites.token': req.query.invite,
}, {
limit: 1,
});

if (count !== 0) {
return next();
}
}

next(new ForbiddenError('Cannot read org logo'));
}),
];
38 changes: 38 additions & 0 deletions src/entities/orgs/handlers/get-org-logo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Request, Response } from 'express';
import { wrapAsyncMiddleware } from '../../../commons/utils/wrap-async-middleware';
import { object } from 'joi';
import { orgExistsGuard } from '../guards/org-exists-guard';
import { params } from '../../../commons/express-joi/params';
import { $id } from '../../../utils/id';
import { Orgs } from '../org';
import { getFilePath } from '../../../storage/get-file-path';
import { NotFoundError } from '../../../commons/errors/not-found-error';
import { canGetOrgLogoGuard } from '../guards/can-get-org-logo-guard';

const validators = [
params(object({
orgId: $id,
})),
];

async function handler(req: Request, res: Response): Promise<void> {
const { orgId } = req.params;

const org = await Orgs().findOne({ _id: orgId });

if (!org.logo) {
throw new NotFoundError('Org has no logo');
}

const filePath = getFilePath(org.logo.id);

res.header('Content-Type', org.logo.type);
res.download(filePath);
}

export const getOrgLogo = [
...orgExistsGuard,
...canGetOrgLogoGuard,
...validators,
wrapAsyncMiddleware(handler),
];
58 changes: 58 additions & 0 deletions src/entities/orgs/handlers/remove-org-logo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Request, Response } from 'express';
import { wrapAsyncMiddleware } from '../../../commons/utils/wrap-async-middleware';
import { emitEvent } from '../../../events/emit-event';
import { object } from 'joi';
import { Orgs } from '../org';
import { serializeOrg } from '../serialize-org';
import { canWriteOrgGuard } from '../guards/can-write-org-guard';
import { orgExistsGuard } from '../guards/org-exists-guard';
import { EventType } from '../../../events/event-type';
import { params } from '../../../commons/express-joi/params';
import { $id } from '../../../utils/id';
import { deleteFile } from '../../../storage/delete-file';
import { BadRequestError } from '../../../commons/errors/bad-request-error';

const validators = [
params(object({
orgId: $id,
})),
];

async function handler(req: Request, res: Response): Promise<void> {
const { orgId } = req.params;

const { logo } = await Orgs().findOne({
_id: orgId,
});

if (!logo) {
throw new BadRequestError('Org has no logo');
}

await deleteFile(logo.id);

await Orgs().updateOne({
_id: orgId,
}, {
$unset: {
logo: 1,
},
});

const org = await Orgs().findOne({
_id: orgId,
});

emitEvent(EventType.org_logo_removed, {
org,
});

res.json(serializeOrg(org));
}

export const removeOrgLogo = [
...orgExistsGuard,
...canWriteOrgGuard,
...validators,
wrapAsyncMiddleware(handler),
];
65 changes: 65 additions & 0 deletions src/entities/orgs/handlers/set-org-logo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Request, Response } from 'express';
import { wrapAsyncMiddleware } from '../../../commons/utils/wrap-async-middleware';
import { emitEvent } from '../../../events/emit-event';
import { object } from 'joi';
import { Orgs } from '../org';
import { serializeOrg } from '../serialize-org';
import { canWriteOrgGuard } from '../guards/can-write-org-guard';
import { orgExistsGuard } from '../guards/org-exists-guard';
import { EventType } from '../../../events/event-type';
import { params } from '../../../commons/express-joi/params';
import { $id } from '../../../utils/id';
import { upload } from '../../../upload';
import { storeFile } from '../../../storage/store-file';
import { deleteFile } from '../../../storage/delete-file';

const validators = [
params(object({
orgId: $id,
})),
];

async function handler(req: Request, res: Response): Promise<void> {
const { orgId } = req.params;
const { file } = req;

const { logo: oldLogo } = await Orgs().findOne({
_id: orgId,
});

const storedFile = await storeFile(file);

await Orgs().updateOne(
{
_id: orgId,
},
{
$set: {
updatedAt: new Date(),
logo: storedFile,
},
},
);

if (oldLogo) {
await deleteFile(oldLogo.id);
}

const org = await Orgs().findOne({
_id: orgId,
});

emitEvent(EventType.org_logo_set, {
org,
});

res.json(serializeOrg(org));
}

export const setOrgLogo = [
...orgExistsGuard,
...canWriteOrgGuard,
upload.single('file'),
...validators,
wrapAsyncMiddleware(handler),
];
2 changes: 2 additions & 0 deletions src/entities/orgs/org.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { AppDb } from '../../db/db';
import { Invite } from './invite';
import { StoredFile } from '../../storage/store-file';

export interface Org {
_id: string;
color: string;
logo?: StoredFile;
createdAt: Date;
updatedAt: Date;
ownerId: string;
Expand Down
32 changes: 32 additions & 0 deletions src/entities/orgs/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { createOrg } from './handlers/create-org';
import { apiEndpoint } from '../api/api-endpoint';
import { ApiScope } from '../api/api-scope';
import { listSites } from './handlers/sites/list-sites';
import { setOrgLogo } from './handlers/set-org-logo';
import { removeOrgLogo } from './handlers/remove-org-logo';
import { getOrgLogo } from './handlers/get-org-logo';

const router = Router();

Expand Down Expand Up @@ -53,6 +56,35 @@ apiEndpoint({
apiScope: ApiScope.org_update,
router,
});

// logo
apiEndpoint({
name: 'set org logo',
method: 'post',
path: '/api/v1/orgs/:orgId/logo',
handler: setOrgLogo,
auth: true,
apiScope: ApiScope.org_logo_set,
router,
});
apiEndpoint({
name: 'remove org logo',
method: 'delete',
path: '/api/v1/orgs/:orgId/logo',
handler: removeOrgLogo,
auth: true,
apiScope: ApiScope.org_logo_remove,
router,
});
apiEndpoint({
name: 'get org logo',
method: 'get',
path: '/api/v1/orgs/:orgId/logo',
handler: getOrgLogo,
auth: true,
apiScope: ApiScope.org_logo_get,
router,
});
// TODO delete

// invites
Expand Down
3 changes: 3 additions & 0 deletions src/entities/orgs/serialize-org.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Org } from './org';
import { env } from '../../env/env';

export function serializeOrg(org: Org) {
return {
_id: org._id,
name: org.name,
color: org.color,
// ?id=... forces cache refresh
logo: org.logo ? `${env.MELI_URL}/api/v1/orgs/${org._id}/logo?id=${org.logo.id}` : undefined,
};
}
38 changes: 38 additions & 0 deletions src/entities/sites/handlers/get-site-logo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Request, Response } from 'express';
import { wrapAsyncMiddleware } from '../../../commons/utils/wrap-async-middleware';
import { object } from 'joi';
import { params } from '../../../commons/express-joi/params';
import { $id } from '../../../utils/id';
import { getFilePath } from '../../../storage/get-file-path';
import { NotFoundError } from '../../../commons/errors/not-found-error';
import { Sites } from '../site';
import { siteExistsGuard } from '../guards/site-exists-guard';
import { canAdminSiteGuard } from '../guards/can-admin-site-guard';

const validators = [
params(object({
siteId: $id,
})),
];

async function handler(req: Request, res: Response): Promise<void> {
const { siteId } = req.params;

const site = await Sites().findOne({ _id: siteId });

if (!site.logo) {
throw new NotFoundError('Site has no logo');
}

const filePath = getFilePath(site.logo.id);

res.header('Content-Type', site.logo.type);
res.download(filePath);
}

export const getSiteLogo = [
...siteExistsGuard,
...canAdminSiteGuard,
...validators,
wrapAsyncMiddleware(handler),
];
Loading

0 comments on commit 0074e42

Please sign in to comment.