Skip to content

Commit bf8d761

Browse files
Merge pull request #135 from uatisdeproblem/development
Development
2 parents 5963963 + c3e46b7 commit bf8d761

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2708
-343
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ back-end/output-config.json
4141
back-end/src/**/*.js
4242
back-end/src/**/*.js.map
4343
front-end/.angular/cache
44-
front-end/resources
44+
front-end/resources
45+
scripts/src/**/*.js

back-end/deploy/main.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const apiResources: ResourceController[] = [
3434
{ name: 'speakers', paths: ['/speakers', '/speakers/{speakerId}'] },
3535
{ name: 'sessions', paths: ['/sessions', '/sessions/{sessionId}'] },
3636
{ name: 'registrations', paths: ['/registrations', '/registrations/{sessionId}'] },
37-
{ name: 'connections', paths: ['/connections', '/connections/{connectionId}'] }
37+
{ name: 'connections', paths: ['/connections', '/connections/{connectionId}'] },
38+
{ name: 'contests', paths: ['/contests', '/contests/{contestId}'] }
3839
];
3940

4041
const tables: { [tableName: string]: DDBTable } = {
@@ -115,6 +116,9 @@ const tables: { [tableName: string]: DDBTable } = {
115116
projectionType: DDB.ProjectionType.ALL
116117
}
117118
]
119+
},
120+
contests: {
121+
PK: { name: 'contestId', type: DDB.AttributeType.STRING }
118122
}
119123
};
120124

back-end/package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

back-end/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "3.4.0",
2+
"version": "3.5.0",
33
"name": "back-end",
44
"scripts": {
55
"lint": "eslint --ext .ts",

back-end/src/handlers/contests.ts

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
///
2+
/// IMPORTS
3+
///
4+
5+
import { DynamoDB, HandledError, ResourceController } from 'idea-aws';
6+
7+
import { Contest } from '../models/contest.model';
8+
import { User } from '../models/user.model';
9+
10+
///
11+
/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER
12+
///
13+
14+
const PROJECT = process.env.PROJECT;
15+
const STAGE = process.env.STAGE;
16+
const DDB_TABLES = { users: process.env.DDB_TABLE_users, contests: process.env.DDB_TABLE_contests };
17+
const ddb = new DynamoDB();
18+
19+
export const handler = (ev: any, _: any, cb: any): Promise<void> => new ContestsRC(ev, cb).handleRequest();
20+
21+
///
22+
/// RESOURCE CONTROLLER
23+
///
24+
25+
class ContestsRC extends ResourceController {
26+
user: User;
27+
contest: Contest;
28+
29+
constructor(event: any, callback: any) {
30+
super(event, callback, { resourceId: 'contestId' });
31+
if (STAGE === 'prod') this.silentLambdaLogs(); // to make the vote anonymous
32+
}
33+
34+
protected async checkAuthBeforeRequest(): Promise<void> {
35+
try {
36+
this.user = new User(await ddb.get({ TableName: DDB_TABLES.users, Key: { userId: this.principalId } }));
37+
} catch (err) {
38+
throw new HandledError('User not found');
39+
}
40+
41+
if (!this.resourceId) return;
42+
43+
try {
44+
this.contest = new Contest(
45+
await ddb.get({ TableName: DDB_TABLES.contests, Key: { contestId: this.resourceId } })
46+
);
47+
} catch (err) {
48+
throw new HandledError('Contest not found');
49+
}
50+
}
51+
52+
protected async getResource(): Promise<Contest> {
53+
if (!this.user.permissions.canManageContents && !this.contest.publishedResults) delete this.contest.results;
54+
return this.contest;
55+
}
56+
57+
protected async putResource(): Promise<Contest> {
58+
if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
59+
60+
const oldResource = new Contest(this.contest);
61+
this.contest.safeLoad(this.body, oldResource);
62+
63+
return await this.putSafeResource();
64+
}
65+
private async putSafeResource(opts: { noOverwrite?: boolean } = {}): Promise<Contest> {
66+
const errors = this.contest.validate();
67+
if (errors.length) throw new HandledError(`Invalid fields: ${errors.join(', ')}`);
68+
69+
const putParams: any = { TableName: DDB_TABLES.contests, Item: this.contest };
70+
if (opts.noOverwrite) putParams.ConditionExpression = 'attribute_not_exists(contestId)';
71+
await ddb.put(putParams);
72+
73+
return this.contest;
74+
}
75+
76+
protected async patchResource(): Promise<void> {
77+
switch (this.body.action) {
78+
case 'VOTE':
79+
return await this.userVote(this.body.candidate);
80+
case 'PUBLISH_RESULTS':
81+
return await this.publishResults();
82+
default:
83+
throw new HandledError('Unsupported action');
84+
}
85+
}
86+
private async userVote(candidateName: string): Promise<void> {
87+
if (!this.contest.isVoteStarted() || this.contest.isVoteEnded()) throw new HandledError('Vote is not open');
88+
89+
if (this.user.isExternal()) throw new HandledError("Externals can't vote");
90+
if (!this.user.spot?.paymentConfirmedAt) throw new HandledError("Can't vote without confirmed spot");
91+
92+
const candidateIndex = this.contest.candidates.findIndex(c => c.name === candidateName);
93+
if (candidateIndex === -1) throw new HandledError('Candidate not found');
94+
95+
const candidateCountry = this.contest.candidates[candidateIndex].country;
96+
if (candidateCountry && candidateCountry === this.user.sectionCountry)
97+
throw new HandledError("Can't vote for your country");
98+
99+
const markUserContestVoted = {
100+
TableName: DDB_TABLES.users,
101+
Key: { userId: this.user.userId },
102+
ConditionExpression: 'attribute_not_exists(votedInContests) OR NOT contains(votedInContests, :contestId)',
103+
UpdateExpression: 'SET votedInContests = list_append(if_not_exists(votedInContests, :emptyArr), :contestList)',
104+
ExpressionAttributeValues: {
105+
':contestId': this.contest.contestId,
106+
':contestList': [this.contest.contestId],
107+
':emptyArr': [] as string[]
108+
}
109+
};
110+
const addUserVoteToContest = {
111+
TableName: DDB_TABLES.contests,
112+
Key: { contestId: this.contest.contestId },
113+
UpdateExpression: `ADD results[${candidateIndex}] :one`,
114+
ExpressionAttributeValues: { ':one': 1 }
115+
};
116+
117+
await ddb.transactWrites([{ Update: markUserContestVoted }, { Update: addUserVoteToContest }]);
118+
}
119+
private async publishResults(): Promise<void> {
120+
if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
121+
122+
if (this.contest.publishedResults) throw new HandledError('Already public');
123+
124+
if (!this.contest.isVoteEnded()) throw new HandledError('Vote is not done');
125+
126+
await ddb.update({
127+
TableName: DDB_TABLES.contests,
128+
Key: { contestId: this.contest.contestId },
129+
UpdateExpression: 'SET publishedResults = :true',
130+
ExpressionAttributeValues: { ':true': true }
131+
});
132+
}
133+
134+
protected async deleteResource(): Promise<void> {
135+
if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
136+
137+
await ddb.delete({ TableName: DDB_TABLES.contests, Key: { contestId: this.resourceId } });
138+
}
139+
140+
protected async postResources(): Promise<Contest> {
141+
if (!this.user.permissions.canManageContents) throw new HandledError('Unauthorized');
142+
143+
this.contest = new Contest(this.body);
144+
this.contest.contestId = await ddb.IUNID(PROJECT);
145+
this.contest.createdAt = new Date().toISOString();
146+
this.contest.enabled = false;
147+
delete this.contest.voteEndsAt;
148+
this.contest.results = [];
149+
this.contest.candidates.forEach((): number => this.contest.results.push(0));
150+
this.contest.publishedResults = false;
151+
152+
return await this.putSafeResource({ noOverwrite: true });
153+
}
154+
155+
protected async getResources(): Promise<Contest[]> {
156+
let contests = (await ddb.scan({ TableName: DDB_TABLES.contests })).map(x => new Contest(x));
157+
158+
if (!this.user.permissions.canManageContents) {
159+
contests = contests.filter(c => c.enabled);
160+
contests.forEach(contest => {
161+
if (!contest.publishedResults) delete contest.results;
162+
});
163+
}
164+
165+
return contests.sort((a, b): number => b.createdAt.localeCompare(a.createdAt));
166+
}
167+
}

0 commit comments

Comments
 (0)