Skip to content

Commit

Permalink
feat: added organizations back-end #78
Browse files Browse the repository at this point in the history
  • Loading branch information
rbento1096 committed Feb 17, 2024
1 parent 348d5a1 commit 5727372
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 1 deletion.
10 changes: 9 additions & 1 deletion back-end/deploy/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const apiResources: ResourceController[] = [
{ name: 'eventSpots', paths: ['/event-spots', '/event-spots/{spotId}'] },
{ name: 'usefulLinks', paths: ['/useful-links', '/useful-links/{linkId}'] },
{ name: 'venues', paths: ['/venues', '/venues/{venueId}'] },
{ name: 'communications', paths: ['/communications', '/communications/{communicationId}'] }
{ name: 'communications', paths: ['/communications', '/communications/{communicationId}'] },
{ name: 'organizations', paths: ['/organizations', '/organizations/{organizationId}'] }
];

const tables: { [tableName: string]: DDBTable } = {
Expand All @@ -50,6 +51,13 @@ const tables: { [tableName: string]: DDBTable } = {
},
communications: {
PK: { name: 'communicationId', type: DDB.AttributeType.STRING }
},
usersReadCommunications: {
PK: { name: 'userId', type: DDB.AttributeType.STRING },
SK: { name: 'communicationId', type: DDB.AttributeType.STRING }
},
organizations: {
PK: { name: 'organizationId', type: DDB.AttributeType.STRING }
}
};

Expand Down
167 changes: 167 additions & 0 deletions back-end/src/handlers/organizations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
///
/// IMPORTS
///

import { DynamoDB, RCError, ResourceController } from 'idea-aws';

import { Organization } from '../models/organization.model';
import { User } from '../models/user.model';

///
/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER
///

const PROJECT = process.env.PROJECT;

const DDB_TABLES = { users: process.env.DDB_TABLE_users, organizations: process.env.DDB_TABLE_organizations };

// @todo add CV stuff?
// const SES_CONFIG = {
// sourceName: 'EGM',
// source: process.env.SES_SOURCE_ADDRESS,
// sourceArn: process.env.SES_IDENTITY_ARN,
// region: process.env.SES_REGION
// };

// const EMAIL_CONTENTS = {
// subject: '[EGM] Contact request',
// textHeader:
// "Hi,\nthis is an automatic email from the EGM app.\n\nI'd like to get in touch with your organization; therefore, here is my contact information:\n\n",
// textAttachment: '\n\nYou can find attached my CV.\n',
// textFooter: '\n\nBest regards,\n'
// };

// const S3_BUCKET_MEDIA = process.env.S3_BUCKET_MEDIA;
// const S3_USERS_CV_FOLDER = process.env.S3_USERS_CV_FOLDER;

const ddb = new DynamoDB();
// const ses = new SES();
// const s3 = new S3();

export const handler = (ev: any, _: any, cb: any) => new Organizations(ev, cb).handleRequest();

///
/// RESOURCE CONTROLLER
///

class Organizations extends ResourceController {
user: User;
organization: Organization;

constructor(event: any, callback: any) {
super(event, callback, { resourceId: 'organizationId' });
}

protected async checkAuthBeforeRequest(): Promise<void> {
try {
this.user = new User(await ddb.get({ TableName: DDB_TABLES.users, Key: { userId: this.principalId } }));
} catch (err) {
throw new RCError('User not found');
}

if (!this.resourceId) return;

try {
this.organization = new Organization(
await ddb.get({ TableName: DDB_TABLES.organizations, Key: { organizationId: this.resourceId } })
);
} catch (err) {
throw new RCError('Organization not found');
}
}

protected async getResource(): Promise<Organization> {
return this.organization;
}

protected async putResource(): Promise<Organization> {
if (!this.user.permissions.canManageContents) throw new RCError('Unauthorized');

const oldResource = new Organization(this.organization);
this.organization.safeLoad(this.body, oldResource);

return await this.putSafeResource();
}
private async putSafeResource(opts: { noOverwrite?: boolean } = {}): Promise<Organization> {
const errors = this.organization.validate();
if (errors.length) throw new RCError(`Invalid fields: ${errors.join(', ')}`);

try {
const putParams: any = { TableName: DDB_TABLES.organizations, Item: this.organization };
if (opts.noOverwrite) putParams.ConditionExpression = 'attribute_not_exists(organizationId)';
await ddb.put(putParams);

return this.organization;
} catch (err) {
throw new RCError('Operation failed');
}
}

// protected async patchResource(): Promise<void> {
// switch (this.body.action) {
// case 'SEND_USER_CONTACTS':
// return await this.sendUserContacts();
// default:
// throw new RCError('Unsupported action');
// }
// }

// @todo?
// private async sendUserContacts(): Promise<void> {
// if (!this.organization.contactEmail) throw new Error('No target email address');
// const userProfile = new UserProfile(
// await ddb.get({
// TableName: DDB_TABLES.profiles,
// Key: { userId: this.cognitoUser.userId }
// })
// );
// if (!userProfile.contactEmail) throw new Error('No source email address');
// const emailData: EmailData = {
// toAddresses: [this.organization.contactEmail],
// replyToAddresses: [userProfile.contactEmail],
// subject: EMAIL_CONTENTS.subject
// };
// let emailText = EMAIL_CONTENTS.textHeader;
// const contactInfo = [userProfile.getName(), userProfile.contactEmail];
// if (this.body.sendPhone) contactInfo.push(userProfile.contactPhone);
// emailText = emailText.concat(contactInfo.map(x => `- ${x}`).join('\n'));
// if (this.body.sendCV && userProfile.hasUploadedCV) {
// const key = S3_USERS_CV_FOLDER.concat('/', this.cognitoUser.userId, '.pdf');
// const { url } = s3.signedURLGet(S3_BUCKET_MEDIA, key);
// emailData.attachments = [{ path: url, filename: 'CV.pdf', contentType: 'application/pdf' }];
// emailText = emailText.concat(EMAIL_CONTENTS.textAttachment);
// }
// emailText = emailText.concat(EMAIL_CONTENTS.textFooter, userProfile.getName());
// emailData.text = emailText;
// await ses.sendEmail(emailData, SES_CONFIG);
// }

protected async deleteResource(): Promise<void> {
if (!this.user.permissions.canManageContents) throw new RCError('Unauthorized');

try {
await ddb.delete({ TableName: DDB_TABLES.organizations, Key: { organizationId: this.resourceId } });
} catch (err) {
throw new RCError('Delete failed');
}
}

protected async postResources(): Promise<Organization> {
if (!this.user.permissions.canManageContents) throw new RCError('Unauthorized');

this.organization = new Organization(this.body);
this.organization.organizationId = await ddb.IUNID(PROJECT);

return await this.putSafeResource({ noOverwrite: true });
}

protected async getResources(): Promise<Organization[]> {
try {
return (await ddb.scan({ TableName: DDB_TABLES.organizations }))
.map((x: Organization) => new Organization(x))
.sort((a, b) => a.name.localeCompare(b.name));
} catch (err) {
throw new RCError('Operation failed');
}
}
}
66 changes: 66 additions & 0 deletions back-end/src/models/organization.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { isEmpty, Resource } from 'idea-toolbox';

export class Organization extends Resource {
/**
* The organization ID.
*/
organizationId: string;
/**
* The name of the organization.
*/
name: string;
/**
* The URI for an image describing the organization
*/
imageURI: string;
/**
* A description of the organization.
*/
description: string;
/**
* The organization's website.
*/
website: string;
/**
* The organization's contact email.
*/
contactEmail: string;
/**
* A link to perform a contact action.
*/
contactAction: string; // @todo check this

load(x: any): void {
super.load(x);
this.organizationId = this.clean(x.organizationId, String);
this.name = this.clean(x.name, String);
this.imageURI = this.clean(x.imageURI, String);
this.description = this.clean(x.description, String);
this.website = this.clean(x.website, String);
this.contactEmail = this.clean(x.contactEmail, String);
this.contactAction = this.clean(x.contactAction, String);
}
safeLoad(newData: any, safeData: any): void {
super.safeLoad(newData, safeData);
this.organizationId = safeData.organizationId;
}
validate(): string[] {
const e = super.validate();
if (isEmpty(this.name)) e.push('name');
if (this.website && isEmpty(this.website, 'url')) e.push('website');
if (this.contactEmail && isEmpty(this.contactEmail, 'email')) e.push('contactEmail');
return e;
}
}

// @todo check this
export class OrganizationLinked extends Resource {
organizationId: string;
name: string;

load(x: any): void {
super.load(x);
this.organizationId = this.clean(x.organizationId, String);
this.name = this.clean(x.name, String);
}
}
101 changes: 101 additions & 0 deletions back-end/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ tags:
description: The venues of the event
- name: Communications
description: The event's communications and announcements.
- name: Organizations
description: The organizations of the event

paths:
/status:
Expand Down Expand Up @@ -675,6 +677,89 @@ paths:
$ref: '#/components/responses/OperationCompleted'
400:
$ref: '#/components/responses/BadParameters'
/organizations:
get:
summary: Get the organizations
tags: [Organizations]
security:
- AuthFunction: []
responses:
200:
$ref: '#/components/responses/Organizations'
post:
summary: Insert a new organization
description: Requires to be content manager
tags: [Organizations]
security:
- AuthFunction: []
requestBody:
required: true
description: Organization
content:
application/json:
schema:
type: object
responses:
200:
$ref: '#/components/responses/Organization'
400:
$ref: '#/components/responses/BadParameters'
/organizations/{organizationId}:
get:
summary: Get an organization
tags: [Organizations]
security:
- AuthFunction: []
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
responses:
200:
$ref: '#/components/responses/Organization'
put:
summary: Edit an organization
description: Requires to be content manager
tags: [Organizations]
security:
- AuthFunction: []
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
requestBody:
required: true
description: Organization
content:
application/json:
schema:
type: object
responses:
200:
$ref: '#/components/responses/Organization'
400:
$ref: '#/components/responses/BadParameters'
delete:
summary: Delete an organization
description: Requires to be content manager
tags: [Organizations]
security:
- AuthFunction: []
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
responses:
200:
$ref: '#/components/responses/OperationCompleted'
400:
$ref: '#/components/responses/BadParameters'

components:
schemas:
Expand Down Expand Up @@ -794,6 +879,22 @@ components:
type: array
items:
$ref: '#/components/schemas/Communication'
Organization:
description: Organization
content:
application/json:
schema:
type: object
items:
$ref: '#/components/schemas/Organization'
Organizations:
description: Organization[]
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Organization'
BadParameters:
description: Bad input parameters
content:
Expand Down

0 comments on commit 5727372

Please sign in to comment.