Skip to content

Commit

Permalink
Refactoring for multiple logins and institutions: Part 2 (#288)
Browse files Browse the repository at this point in the history
* Make user-facing UID name configurable

* Update schema and create migration for rename to uid

* Transition to UID instead of NetID

* Fix all tests

* Add autocomplete to question email

* Fix broken tests

* Fix type error

* Allow creating new users in new question panel

* Capitalize uid name on profile page

* Filter existing staff from staff autocompletion

* Fix linter errors

* Fix ts-node options

* Append @illinois.edu to the end of all uids

* Upgrade MySQL APT Config

* Specify MySQL collation

* Use utf8mb4

* Specify charset in node

* Switch back to utf8mb4

* Switch to utf8

* Add unique constraint

* Remove unique constraint before renaming column

* Remove constraint under MySQL

* Update CHANGELOG.md

* Source environment variables using dotenv

* Specify dialect when creating migration/verification databases

* Fix uid in tests

* Rename columns

* Move env variables outside of build environment

* Add documentation for .env

* Add broader Shibboleth compatibility for the eppn attribute

* Expose eppn suffix check as an environment variable

* Add EPPN_SUFFIX to now for consistency

* Get rid of @ in EPPN_SUFFIX for now

Now interprets environment variables prefixed
with an "@" to be a secret

Co-authored-by: Nathan Walters <[email protected]>
Co-authored-by: Wade Fagen-Ulmschneider <[email protected]>
  • Loading branch information
3 people authored Feb 20, 2020
1 parent f3bb773 commit e4d8704
Show file tree
Hide file tree
Showing 52 changed files with 1,183 additions and 974 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ When a new version is tagged, the changes since the last deploy should be labele
with the current semantic version and the next changes should go under a **[Next]** header.

## [Next]
* Refactor authentication to support multiple logins and institutions. ([@nwalters512](https://github.com/nwalters512) in [#283](https://github.com/illinois/queue/pull/283))

* Update API doc ([@jackieo5023](https://github.com/jackieo5023) in [#303](https://github.com/illinois/queue/pull/303))

Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ If you wish to report a bug, feature request, etc., please open a new issue (fir

Run `npm run build` to build assets for production; read more about the build process [in the docs](docs/Build.md). Run `npm run start` to start the application.

Several configuration options are exposed via environment variables:
Several configuration options are exposed via environment variables and `.env`:

- `PORT`: controls which port the app will be served from.
- `BASE_URL`: allows the app to be served from somewhere other than the server
Expand All @@ -51,3 +51,28 @@ Several configuration options are exposed via environment variables:
`/my/path/`, then you should run with `BASE_URL=/my/path` (note the lack of
trailing slash), and a request for queue 1 should be received as `/my/path/queue/1`.
- `JWT_SECRET`: a secret key used to sign JSON Web Tokens for our users.
- `UID_NAME`: official name for user ids (default: `email`)
- `UID_ARTICLE`: article used in reference to `UID_NAME` (default: `an`)
- `EPPN_SUFFIX`: the expected suffix for all valid Shibboleth eppn attributes. If this variable is not present, then all Shibboleth responses will be accepted.

Database information is also stored through environment variables, and can be configured for multiple environments depending on the value of `NODE_ENV`.
```
DB_USERNAME_<ENV>="username"
DB_PASSWORD_<ENV>="password"
DB_DATABASE_<ENV>="database_name"
DB_HOST_<ENV>="localhost"
DB_DIALECT_<ENV>="sqlite"
DB_LOGGING_<ENV>="false"
DB_STORAGE_<ENV>="./dev.sqlite" # Sqlite only
# Example for development:
DB_USERNAME_DEVELOPMENT="username"
DB_PASSWORD_DEVELOPMENT="password"
DB_DATABASE_DEVELOPMENT="queue"
DB_HOST_DEVELOPMENT="localhost"
DB_DIALECT_DEVELOPMENT="sqlite"
DB_LOGGING_DEVELOPMENT="false"
DB_STORAGE_DEVELOPMENT="./dev.sqlite" # Sqlite only
```

Note that for SQLite databases, only `DB_STORAGE_<ENV>` is required.
6 changes: 6 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ const withCSS = require('@zeit/next-css')
const withSass = require('@zeit/next-sass')
const withTypescript = require('@zeit/next-typescript')

require('dotenv').config()

module.exports = withTypescript(
withSass(
withCSS({
useFileSystemPublicRoutes: false,
assetPrefix: process.env.BASE_URL || '',
publicRuntimeConfig: {
uidName: process.env.UID_NAME || 'email',
uidArticle: process.env.UID_ARTICLE || 'an',
},
})
)
)
2 changes: 1 addition & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"watch": ["src/**/*"],
"execMap": {
"ts": "ts-node --typeCheck --compilerOptions '{\"module\":\"commonjs\"}'"
"ts": "ts-node --type-check --compiler-options '{\"module\":\"commonjs\"}'"
}
}
8 changes: 7 additions & 1 deletion now.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@
"max": 1
}
},
"public": true
"public": true,
"env": {
"DB_DIALECT_NOW": "sqlite",
"DB_STORAGE_NOW": "/tmp/now.sqlite",
"DB_LOGGING_NOW": "false",
"EPPN_SUFFIX": "illinois.edu"
}
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"start": "NODE_ENV=production node build/server.js",
"now-start": "NODE_ENV=now node build/server.js",
"test": "NODE_ENV=test jest",
"test-sequential": "jest --runInBand",
"test-sequential": "NODE_ENV=test jest --runInBand",
"lint": "eslint --ext .js,.jsx,.ts,.tsx ./src",
"fix-lint": "npm run lint -- --fix",
"typecheck": "npx tsc --noEmit",
Expand Down Expand Up @@ -142,6 +142,9 @@
"transform": {
"^.+\\.tsx?$": "ts-jest",
"^.+\\.jsx?$": "babel-jest"
},
"moduleNameMapper": {
"\\.(css|less|sass|scss)$": "<rootDir>/src/test/styleMock.js"
}
},
"husky": {
Expand Down
28 changes: 11 additions & 17 deletions src/actions/course.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ export function fetchCourse(courseId) {
const addCourseStaffRequest = makeActionCreator(
types.ADD_COURSE_STAFF.REQUEST,
'courseId',
'netid',
'name'
'userId'
)
const addCourseStaffSuccess = makeActionCreator(
types.ADD_COURSE_STAFF.SUCCESS,
Expand All @@ -140,22 +139,17 @@ const addCourseStaffFailure = makeActionCreator(
'data'
)

export function addCourseStaff(courseId, netid, name) {
export function addCourseStaff(courseId, userId) {
return dispatch => {
dispatch(addCourseStaffRequest(courseId, netid, name))

return axios
.post(`/api/courses/${courseId}/staff`, {
netid,
name,
})
.then(
res => dispatch(addCourseStaffSuccess(courseId, res.data)),
err => {
console.error(err)
dispatch(addCourseStaffFailure(err))
}
)
dispatch(addCourseStaffRequest(courseId, userId))

return axios.put(`/api/courses/${courseId}/staff/${userId}`).then(
res => dispatch(addCourseStaffSuccess(courseId, res.data)),
err => {
console.error(err)
dispatch(addCourseStaffFailure(err))
}
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/api/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ router.get(
const users = await User.findAll({
where: {
[Sequelize.Op.or]: {
netid: {
uid: {
[Sequelize.Op.like]: `${q}%`,
},
},
Expand Down
14 changes: 7 additions & 7 deletions src/api/autocomplete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,35 @@ afterEach(() => testutil.destroyTestDb())

describe('Autocomplete API', () => {
it('returns all users for empty query', async () => {
const request = await requestAsUser(app, 'admin')
const request = await requestAsUser(app, 'admin@illinois.edu')
const res = await request.get('/api/autocomplete/users?q=')
expect(res.statusCode).toBe(200)
expect(res.body).toHaveLength(7)
expect(res.body[0]).toMatchObject({
netid: 'dev',
uid: 'dev@illinois.edu',
isAdmin: true,
})
})

it('returns specific user for partial netid', async () => {
const request = await requestAsUser(app, 'admin')
it('returns specific user for partial uid', async () => {
const request = await requestAsUser(app, 'admin@illinois.edu')
const res = await request.get('/api/autocomplete/users?q=225')
expect(res.statusCode).toBe(200)
expect(res.body).toHaveLength(1)
expect(res.body[0]).toMatchObject({
netid: '225staff',
uid: '225staff@illinois.edu',
name: '225 Staff',
})
})

it('disallows requests from course staff', async () => {
const request = await requestAsUser(app, '225staff')
const request = await requestAsUser(app, '225staff@illinois.edu')
const res = await request.get('/api/autocomplete/users?q=225')
expect(res.statusCode).toBe(403)
})

it('disallows requests from student', async () => {
const request = await requestAsUser(app, 'student')
const request = await requestAsUser(app, 'student@illinois.edu')
const res = await request.get('/api/autocomplete/users?q=225')
expect(res.statusCode).toBe(403)
})
Expand Down
32 changes: 10 additions & 22 deletions src/api/courses.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ const getCsv = questions => {
'comments',
'preparedness',
'UserLocation',
'answeredBy.AnsweredBy_netid',
'answeredBy.AnsweredBy_uid',
'answeredBy.AnsweredBy_UniversityName',
'askedBy.AskedBy_netid',
'askedBy.AskedBy_uid',
'askedBy.AskedBy_UniversityName',
'queue.queueId',
'queue.courseId',
Expand Down Expand Up @@ -113,7 +113,7 @@ router.get(
includes.push({
model: User,
as: 'staff',
attributes: ['id', 'netid', 'name'],
attributes: ['id', 'uid', 'name'],
through: {
attributes: [],
},
Expand Down Expand Up @@ -179,7 +179,7 @@ router.get(
model: User,
as: 'askedBy',
attributes: [
['netid', 'AskedBy_netid'],
['uid', 'AskedBy_uid'],
['universityName', 'AskedBy_UniversityName'],
],
required: true,
Expand All @@ -189,7 +189,7 @@ router.get(
model: User,
as: 'answeredBy',
attributes: [
['netid', 'AnsweredBy_netid'],
['uid', 'AnsweredBy_uid'],
['universityName', 'AnsweredBy_UniversityName'],
],
required: false,
Expand Down Expand Up @@ -302,20 +302,11 @@ router.patch(
)

// Add someone to course staff
router.post(
'/:courseId/staff',
[
requireCourseStaff,
requireCourse,
check('netid', 'netid must be specified')
.exists()
.trim(),
failIfErrors,
],
router.put(
'/:courseId/staff/:userId',
[requireCourseStaff, requireCourse, requireUser, failIfErrors],
safeAsync(async (req, res, _next) => {
const { netid: originalNetid } = matchedData(req)
const [netid] = originalNetid.split('@')
const [user] = await User.findOrCreate({ where: { netid } })
const { user } = res.locals
await user.addStaffAssignment(res.locals.course.id)
res.status(201).send(user)
})
Expand All @@ -327,10 +318,7 @@ router.delete(
[requireCourseStaff, requireCourse, requireUser, failIfErrors],
safeAsync(async (req, res, _next) => {
const { user, course } = res.locals
const oldStaffAssignments = await user.getStaffAssignments()
await user.setStaffAssignments(
oldStaffAssignments.filter(c => c.id !== course.id)
)
await user.removeStaffAssignment(course)
res.status(202).send()
})
)
Expand Down
Loading

0 comments on commit e4d8704

Please sign in to comment.