Skip to content

Commit d5c224a

Browse files
Sequelize update & readme corrections (#113)
* Update sequelize to v5 (#110) * Update sequelize and use [Op.exp] instead of for sequelize expressions * Use findByPk instead of findById * Fix import of sequelize in config * Fix updateAttributes to update, as the former has been deprecated in favor of the latter * Update pg version to support sequelize v5 * Fix up expected errors in tests * Fix tests to match new sequelize version * Fix tag counts via dedupe * Handle slashes in go links (#109) Problem: If a go-link has a / in its name, any PUT/DELETEs referencing it will fail, as the / causes ExpressJS to interpret the request as a separate route. Solution: By using a wildcard regxp for the route path (https://expressjs.com/en/guide/routing.html#route-paths) eg: /:shortLink(*) ExpressJS will glob anything after the matching part of the route, handling the remainder appropriately. req.params.shortLink will still return back the whole go-link, so this should have no impact on other links, even if they also start with the same prefix/ * Readme corrections (#91) * Readme corrections * Ignore intellij files * Add note about environment * Remove env comment * Add announcements (#114) * Add announcements migration and corresponding rest endpoints / integration tests * Update names of announcement properties * Fix cascade delete quotes tags migration to await on temp table creation (#116) Co-authored-by: Ben Alderfer <[email protected]>
1 parent ef75c7c commit d5c224a

36 files changed

+874
-340
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,6 @@ config/database/production.json
6767

6868
# vim
6969
*.swp
70+
71+
# intellij
72+
.idea

README.md

100644100755
Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ The following steps walk you through configuring Google OAuth, with the assumpti
2929

3030
1. Navigate to [Google Developer Console](https://console.developers.google.com/project), making sure you are logged in to the Google account you would like associated with the OAuth Authentication.
3131
2. Select the option to create a new project, naming it whatever you wish. Click 'Create'. You should be redirected to your Project's API Manager page.
32-
3. Open the Credentials page by selecting it from the navigation pane on the left-hand side of the page.
32+
3. Open the Credentials page by selecting it from the navigation pane on the left-hand side of the page (hamburger menu > APIs & Services > Credentials).
3333
4. On the Credentials page, select the option 'Create credentials', and then 'OAuth client ID'.
34-
5. You should now see a warning that you must set a product name on the consent screen. Select the option to 'Configure consent screen'. You will now be taken to the 'OAuth consent screen' settings pane. On this page, fill in the 'Product name shown to users' field with a name identifiable to your users. `SSE Dev API` is a good choice. When you're ready, click 'Save'.
34+
5. You should now see a warning that you must set a product name on the consent screen. Select the option to 'Configure consent screen'. You will now be taken to the 'OAuth consent screen' settings pane. On this page, fill in the 'Application name' field with a name identifiable to your users. `SSE Dev API` is a good choice. When you're ready, click 'Save'.
3535
6. Next, you'll be guided through the process of creating your OAuth Client Credentials. First, select 'Web application' as the Application type. Next, fill in the following information on the form that appears:
3636
- **Name:** `SSE Dev API` (or however you'd like to refer to it internally)
3737
- **Authorized JavaScript origins:** `http://localhost:5000`
@@ -65,16 +65,6 @@ You only need to do this if you are working on mentoring-related endpoints.
6565
* Head back to [Google Developer Console](https://console.developers.google.com/apis/library). Go to the same project you created before. Go to Libraries and search for Google Calendar API. Enable that API.
6666
* Next go to Credentials Tab. Click Create Credentials > API Key. You can store this key in `keys/google.json` under `web.api_key` or set the ENV variable `GOOGLE_API_KEY`.
6767

68-
### Google Calendar
69-
Only need to do this if you are working on mentoring-related endpoints.
70-
71-
* Create a new Google Calendar for the Mentor Schedule
72-
* In the side bar, Click on the arrow next to your new calendar and go to calendar settings.
73-
* On the Calendar Details screen, you will find your calendar ID in the *Calendar Address* section. You can store this value in `keys/google.json` under `web.calendars.mentor`, or use the ENV variable `MENTOR_GOOGLE_CALENDAR`.
74-
* Go to the Share Calendar Tab and make the calendar public.
75-
* Head back to [Google Developer Console](https://console.developers.google.com/apis/library). Go to the same project you created before. Go to Libraries and search for Google Calendar API. Enable that API.
76-
* Next go to Credentials Tab. Click Create Credentials > API Key. You can store this key in `keys/google.json` under `web.api_key` or set the ENV variable `GOOGLE_API_KEY`.
77-
7868
### Configuring Mailgun
7969
Only need to do this if you are working on scoreboard-related endpoints.
8070

@@ -89,7 +79,7 @@ Only need to do this if you are working on scoreboard-related endpoints.
8979
1. `npm install`
9080
2. `mkdir keys`
9181
3. `npm run keygen`
92-
4. `npm run bootstrap -- --admin:firstName [YOUR NAME] --admin:lastName [YOUR LAST NAME] --admin:dce [YOUR DCE] --keygen --seed` - Creates and migrates the database. If you specify the admin args, a membership will be created for that
82+
4. `npm run bootstrap -- --admin:firstName [YOUR NAME] --admin:lastName [YOUR LAST NAME] --admin:dce [YOUR DCE (ex: abc1234)] --keygen --seed` - Creates and migrates the database. If you specify the admin args, a membership will be created for that
9383
user with all permissions. If you specify keygen, all keys will be regenerated.
9484
If you specify seed it will seed the database.
9585
5. `npm start`

config/permissions.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// their permissions level and group assignment have to allow them to do so.
55
//
66
// Permission level is 'at least', so someone with
7-
// a 'low' permission cannot do 'high' permisison actions but
7+
// a 'low' permission cannot do 'high' permission actions but
88
// someone with a 'high' permission can do 'low' permission actions.
99
// Permission level is based on the provider they used to authenticate (eg. Google, Slack).
1010
//
@@ -157,4 +157,18 @@ export default {
157157
groups: { primary, officers },
158158
},
159159
},
160+
announcements: {
161+
create: {
162+
level: levels.high,
163+
groups: { primary },
164+
},
165+
update: {
166+
level: levels.high,
167+
groups: { primary },
168+
},
169+
destroy: {
170+
level: levels.high,
171+
groups: { primary },
172+
},
173+
},
160174
};

config/sequelize.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Sequelize from 'sequelize';
1+
import { Sequelize } from 'sequelize';
22
import nconf from './index';
33

44
const env = nconf.get('NODE_ENV');

db/migrations/20171231073953-cascade-delete-quotes-tags.js

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// Add ON DELETE CASCADE and ON UPDATE CASCADE to the quotes_tags table
2-
export function up(queryInterface, Sequelize) {
2+
export async function up(queryInterface, Sequelize) {
33
const dialect = queryInterface.sequelize.getDialect();
44
// SQLite does not support the DROP CONSTRAINT syntax,
55
// so we have to make a temporary table, copy all the data into it,
66
// and then rename it to the original table. Since we use SQLite in
77
// development and testing with small amounts of data, while this is
88
// gross, it is not necessarily the end of the world.
99
if (dialect === 'sqlite') {
10-
return queryInterface.sequelize.transaction(t => Promise.all([
11-
queryInterface.createTable('temp', {
10+
return queryInterface.sequelize.transaction(async (t) => {
11+
await queryInterface.createTable('temp', {
1212
tagName: {
1313
type: Sequelize.STRING,
1414
allowNull: false,
@@ -33,17 +33,20 @@ export function up(queryInterface, Sequelize) {
3333
},
3434
createdAt: Sequelize.DATE,
3535
updatedAt: Sequelize.DATE,
36-
}, { transaction: t }),
37-
queryInterface
38-
.sequelize
39-
.query('INSERT INTO temp SELECT * FROM quotes_tags;', { transaction: t }),
40-
queryInterface
41-
.sequelize
42-
.query('DROP TABLE quotes_tags;', { transaction: t }),
43-
queryInterface
44-
.sequelize
45-
.query('ALTER TABLE temp RENAME TO quotes_tags;', { transaction: t }),
46-
]));
36+
}, {transaction: t});
37+
38+
return Promise.all([
39+
queryInterface
40+
.sequelize
41+
.query('INSERT INTO temp SELECT * FROM quotes_tags;', {transaction: t}),
42+
queryInterface
43+
.sequelize
44+
.query('DROP TABLE quotes_tags;', {transaction: t}),
45+
queryInterface
46+
.sequelize
47+
.query('ALTER TABLE temp RENAME TO quotes_tags;', {transaction: t}),
48+
]);
49+
});
4750
} else if (dialect === 'postgres') {
4851
return queryInterface.sequelize.transaction(t => Promise.all([
4952
queryInterface
@@ -71,12 +74,12 @@ export function up(queryInterface, Sequelize) {
7174
}
7275

7376
// Remove ON DELETE CASCADE and ON UPDATE CASCADE FROM the quotes_tags table
74-
export function down(queryInterface, Sequelize) {
77+
export async function down(queryInterface, Sequelize) {
7578
const dialect = queryInterface.sequelize.getDialect();
7679
// See above comment about SQLite
7780
if (dialect === 'sqlite') {
78-
return queryInterface.sequelize.transaction(t => Promise.all([
79-
queryInterface.createTable('temp', {
81+
return queryInterface.sequelize.transaction(async (t) => {
82+
await queryInterface.createTable('temp', {
8083
tagName: {
8184
type: Sequelize.STRING,
8285
allowNull: false,
@@ -91,17 +94,20 @@ export function down(queryInterface, Sequelize) {
9194
},
9295
createdAt: Sequelize.DATE,
9396
updatedAt: Sequelize.DATE,
94-
}, { transaction: t }),
95-
queryInterface
96-
.sequelize
97-
.query('INSERT INTO temp SELECT * FROM quotes_tags;', { transaction: t }),
98-
queryInterface
99-
.sequelize
100-
.query('DROP TABLE quotes_tags;', { transaction: t }),
101-
queryInterface
102-
.sequelize
103-
.query('ALTER TABLE temp RENAME TO quotes_tags;', { transaction: t }),
104-
]));
97+
}, { transaction: t });
98+
99+
return Promise.all([
100+
queryInterface
101+
.sequelize
102+
.query('INSERT INTO temp SELECT * FROM quotes_tags;', { transaction: t }),
103+
queryInterface
104+
.sequelize
105+
.query('DROP TABLE quotes_tags;', { transaction: t }),
106+
queryInterface
107+
.sequelize
108+
.query('ALTER TABLE temp RENAME TO quotes_tags;', { transaction: t }),
109+
]);
110+
});
105111
} else if (dialect === 'postgres') {
106112
return queryInterface.sequelize.transaction(t => Promise.all([
107113
queryInterface
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export function up(queryInterface, Sequelize) {
2+
return queryInterface.createTable('announcements', {
3+
id: {
4+
type: Sequelize.INTEGER,
5+
primaryKey: true,
6+
autoIncrement: true,
7+
},
8+
message: {
9+
type: Sequelize.TEXT,
10+
allowNull: false,
11+
},
12+
category: Sequelize.STRING,
13+
active: {
14+
type: Sequelize.BOOLEAN,
15+
allowNull: false,
16+
},
17+
createdAt: Sequelize.DATE,
18+
updatedAt: Sequelize.DATE,
19+
});
20+
}
21+
22+
export function down(queryInterface) {
23+
return queryInterface.dropTable('announcements');
24+
}

events/membership.js

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,55 +25,64 @@ const sendCongratsEmail = (user, secretary = null) => mailer.sendMail({
2525
});
2626

2727
// If this is the first membership someone receives during the semester, send them a congrats email
28-
Membership.afterUpdate((membership) => {
29-
const previousApproved = membership.previous('approved');
30-
// Attempt to send email if this membership is being approved
31-
if (membership.approved && (previousApproved === null || previousApproved === false)) {
32-
// Count all active approved memberships for this user
33-
Membership
34-
.scope([
35-
{ method: ['user', membership.userDce] },
36-
{ method: ['approved', true] },
37-
{ method: ['active', moment().toISOString()] },
38-
])
39-
.count()
40-
.then((count) => {
41-
// If there is only 1, then we can assume this is the first approved membership
42-
// this person has received this semester, so we'll send them an email.
43-
//
44-
// Edge cases:
45-
// 1. A User had 1 membership. It was deleted. They receieved another membership. They would receive a second email.
46-
// 2. A User had 1 membership. It was unapproved. It was approved. They would receive a second email.
47-
//
48-
// We're fine with both cases because they're infrequent, if they occur at all
49-
// – a user would be reminded of membership details which is ¯\_(ツ)_/¯
50-
// Additionally, the only people who can approve memberships are primary officers,
51-
// so we can trust that they won't abuse their power and spam someone's inbox.
52-
if (count === 1) {
53-
Promise
54-
.all([
55-
// Reload the membership to get the User associated with it
56-
membership.reload({ include: [User] }),
57-
// Find the current SSE Secretary
58-
Officer
59-
.scope([
60-
{ method: ['title', 'Secretary'] },
61-
{ method: ['active', moment().toISOString()] },
62-
])
63-
.findAll({ include: [User] }),
64-
])
65-
.then((values) => {
66-
const user = values[0].user;
67-
const secretary = values[1][0].user;
68-
// Don't send emails during testing
69-
if (config.get('NODE_ENV') !== 'test') {
70-
console.log(`${moment().toISOString()}: Sending membership email to ${user.dce}@rit.edu`); // eslint-disable-line no-console
71-
sendCongratsEmail(user, secretary);
72-
}
73-
});
74-
}
75-
});
76-
}
77-
});
28+
Membership.hooks = {
29+
afterUpdate: (membership) => {
30+
const previousApproved = membership.previous('approved');
31+
// Attempt to send email if this membership is being approved
32+
if (membership.approved && (previousApproved === null || previousApproved === false)) {
33+
// Count all active approved memberships for this user
34+
Membership
35+
.scope([
36+
{ method: ['user', membership.userDce] },
37+
{ method: ['approved', true] },
38+
{
39+
method: ['active', moment()
40+
.toISOString()],
41+
},
42+
])
43+
.count()
44+
.then((count) => {
45+
// If there is only 1, then we can assume this is the first approved membership
46+
// this person has received this semester, so we'll send them an email.
47+
//
48+
// Edge cases:
49+
// 1. A User had 1 membership. It was deleted. They received another membership. They would receive a second email.
50+
// 2. A User had 1 membership. It was unapproved. It was approved. They would receive a second email.
51+
//
52+
// We're fine with both cases because they're infrequent, if they occur at all
53+
// – a user would be reminded of membership details which is ¯\_(ツ)_/¯
54+
// Additionally, the only people who can approve memberships are primary officers,
55+
// so we can trust that they won't abuse their power and spam someone's inbox.
56+
if (count === 1) {
57+
Promise
58+
.all([
59+
// Reload the membership to get the User associated with it
60+
membership.reload({ include: [User] }),
61+
// Find the current SSE Secretary
62+
Officer
63+
.scope([
64+
{ method: ['title', 'Secretary'] },
65+
{
66+
method: ['active', moment()
67+
.toISOString()],
68+
},
69+
])
70+
.findAll({ include: [User] }),
71+
])
72+
.then((values) => {
73+
const user = values[0].user;
74+
const secretary = values[1][0].user;
75+
// Don't send emails during testing
76+
if (config.get('NODE_ENV') !== 'test') {
77+
console.log(`${moment()
78+
.toISOString()}: Sending membership email to ${user.dce}@rit.edu`); // eslint-disable-line no-console
79+
sendCongratsEmail(user, secretary);
80+
}
81+
});
82+
}
83+
});
84+
}
85+
},
86+
};
7887

7988
export default Membership;

middleware/permissions.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import User from '../models/user';
33
export function needs(endpoint, action) {
44
return (req, res, next) => {
55
User
6-
.findById(req.auth.user.dce)
6+
.findByPk(req.auth.user.dce)
77
.then(user => user.can(endpoint, action, req.auth.level))
88
.then(() => next())
99
.catch(() => next({
@@ -35,7 +35,7 @@ export function needsApprovedIndex(endpoint) {
3535
}
3636

3737
User
38-
.findById(req.auth.user.dce)
38+
.findByPk(req.auth.user.dce)
3939
.then(user => user.can(endpoint, 'unapproved', req.auth.level))
4040
.then(() => next())
4141
.catch(() => {
@@ -54,7 +54,7 @@ export function needsApprovedOne(endpoint) {
5454
return next();
5555
}
5656
User
57-
.findById(req.auth.user.dce)
57+
.findByPk(req.auth.user.dce)
5858
.then(user => user.can(endpoint, 'unapproved', req.auth.level))
5959
.then(() => {
6060
req.auth.allowed = true;

models/announcement.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import DataTypes from 'sequelize';
2+
import sequelize from '../config/sequelize';
3+
import paginate from '../helpers/paginate';
4+
import sorting from '../helpers/sorting';
5+
6+
export default sequelize.define('announcements', {
7+
message: {
8+
type: DataTypes.STRING,
9+
allowNull: false,
10+
},
11+
category: DataTypes.STRING,
12+
active: {
13+
type: DataTypes.BOOLEAN,
14+
allowNull: false,
15+
},
16+
}, {
17+
scopes: {
18+
onlyActive() {
19+
return { where: { active: true } };
20+
},
21+
paginate,
22+
orderBy(field, direction) {
23+
return sorting(field, direction, [
24+
'message',
25+
'category',
26+
'active',
27+
'createdAt',
28+
'updatedAt',
29+
]);
30+
},
31+
},
32+
});
33+

0 commit comments

Comments
 (0)