Skip to content

Commit

Permalink
Rework backend invitations api
Browse files Browse the repository at this point in the history
Add timestamp and unique constraint (combination of roadmapId & email)
to invitations. Add route for getting invitations.
  • Loading branch information
II-KA committed Oct 27, 2021
1 parent 5d7b2fd commit 3875576
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 29 deletions.
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"koa-bodyparser": "^4.3.0",
"koa-passport": "^4.1.3",
"koa-session": "^6.0.0",
"moment": "^2.29.1",
"node-rsa": "^1.1.1",
"oauth": "^0.9.15",
"objection": "^2.2.15",
Expand Down
42 changes: 21 additions & 21 deletions server/src/api/invitations/invitations.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RouteHandlerFnc } from 'src/types/customTypes';
import moment from 'moment';
import Invitation from './invitations.model';
import User from '../users/users.model';
import uuid from 'uuid';
Expand All @@ -7,7 +8,15 @@ import { sendEmail } from '../../utils/sendEmail';
// should FRONTEND_BASE_URL and CORS_ORIGIN be same variable?
const BASE_URL = process.env.FRONTEND_BASE_URL!;

export const addInvitations: RouteHandlerFnc = async (ctx) => {
export const getInvitations: RouteHandlerFnc = async (ctx) => {
ctx.body = await Invitation.query()
.where({
roadmapId: Number(ctx.params.roadmapId),
})
.where('updatedAt', '>=', moment().subtract(1, 'month').toDate());
};

export const postInvitation: RouteHandlerFnc = async (ctx) => {
const { type, email, ...others } = ctx.request.body;
if (Object.keys(others).length) return void (ctx.status = 400);

Expand All @@ -20,24 +29,15 @@ export const addInvitations: RouteHandlerFnc = async (ctx) => {

if (existingRole) throw new Error('Invitee is already a team member');

const previousInvitation = await Invitation.query()
.where({ email, roadmapId })
.first();

if (previousInvitation) {
const updated = await Invitation.query().patchAndFetchById(
previousInvitation.id,
{ type },
);
return void (ctx.body = updated);
}

const created = await Invitation.query().insertAndFetch({
id: uuid.v4(),
roadmapId,
type,
email,
});
const created = await Invitation.query()
.insertAndFetch({
id: uuid.v4(),
roadmapId,
type,
email,
})
.onConflict(['roadmapId', 'email'])
.merge();
await sendEmail(
email,
'Invitation to roadmap',
Expand All @@ -46,7 +46,7 @@ export const addInvitations: RouteHandlerFnc = async (ctx) => {
return void (ctx.body = created);
};

export const patchInvitations: RouteHandlerFnc = async (ctx) => {
export const patchInvitation: RouteHandlerFnc = async (ctx) => {
const { id, type, email, roadmapId, ...others } = ctx.request.body;
if (Object.keys(others).length) return void (ctx.status = 400);

Expand All @@ -62,7 +62,7 @@ export const patchInvitations: RouteHandlerFnc = async (ctx) => {
}
};

export const deleteInvitations: RouteHandlerFnc = async (ctx) => {
export const deleteInvitation: RouteHandlerFnc = async (ctx) => {
const numDeleted = await Invitation.query()
.where({
id: ctx.params.invitationId,
Expand Down
20 changes: 19 additions & 1 deletion server/src/api/invitations/invitations.model.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Model } from 'objection';
import { Model, Pojo } from 'objection';
import moment from 'moment';
import { RoleType } from '../../../../shared/types/customTypes';

export default class Invitation extends Model {
id!: string;
roadmapId!: number;
type!: RoleType;
email!: string;
updatedAt!: Date;

valid?: boolean;

static tableName = 'invitations';

Expand All @@ -18,4 +22,18 @@ export default class Invitation extends Model {
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
},
};

$beforeInsert() {
this.updatedAt = new Date();
}
$beforeUpdate() {
this.updatedAt = new Date();
}
$parseDatabaseJson(json: Pojo) {
json = super.$parseDatabaseJson(json);
const date = json.updatedAt && new Date(json.updatedAt);
json.updatedAt = date;
json.valid = date >= moment().subtract(48, 'hours').toDate();
return json;
}
}
19 changes: 13 additions & 6 deletions server/src/api/invitations/invitations.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,36 @@ import { Context } from 'koa';
import { IKoaState } from '../../types/customTypes';
import { Permission } from '../../../../shared/types/customTypes';
import {
addInvitations,
patchInvitations,
deleteInvitations,
getInvitations,
postInvitation,
patchInvitation,
deleteInvitation,
} from './invitations.controller';

const invitationsRouter = new KoaRouter<IKoaState, Context>();

invitationsRouter.get(
'/invitations',
requirePermission(Permission.RoadmapInvite),
getInvitations,
);

invitationsRouter.post(
'/invitations',
requirePermission(Permission.RoadmapInvite),
addInvitations,
postInvitation,
);

invitationsRouter.patch(
'/invitations/:invitationId',
requirePermission(Permission.RoadmapInvite),
patchInvitations,
patchInvitation,
);

invitationsRouter.delete(
'/invitations/:invitationId',
requirePermission(Permission.RoadmapInvite),
deleteInvitations,
deleteInvitation,
);

export default invitationsRouter;
6 changes: 5 additions & 1 deletion server/src/api/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import passport from 'passport';
import uuid from 'uuid';
import moment from 'moment';
import { RouteHandlerFnc } from '../../types/customTypes';
import User from './users.model';
import Invitation from '../invitations/invitations.model';
Expand Down Expand Up @@ -152,7 +153,10 @@ export const getUserRoles: RouteHandlerFnc = async (ctx) => {
export const joinRoadmap: RouteHandlerFnc = async (ctx) => {
if (!ctx.state.user) throw new Error('User is required');
const { ...others } = ctx.request.body;
const invitation = await Invitation.query().findById(ctx.params.invitationId);

const invitation = await Invitation.query()
.findById(ctx.params.invitationId)
.where('updatedAt', '>=', moment().subtract(48, 'hours').toDate());

if (Object.keys(others).length || !invitation) return void (ctx.status = 400);
if (invitation.email !== ctx.state.user.email) return void (ctx.status = 403);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as Knex from 'knex';

export async function up(knex: Knex): Promise<void> {
return knex.schema.alterTable('invitations', (table) => {
table.timestamp('updatedAt');
table.unique(['roadmapId', 'email']);
});
}

export async function down(knex: Knex): Promise<void> {
return knex.schema.alterTable('invitations', (table) => {
table.dropColumn('updatedAt');
table.dropUnique(['roadmapId', 'email']);
});
}
5 changes: 5 additions & 0 deletions server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3722,6 +3722,11 @@ mocha@^9.0.2:
yargs-parser "20.2.4"
yargs-unparser "2.0.0"

moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==

ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
Expand Down

0 comments on commit 3875576

Please sign in to comment.