Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2024 03 01 dep update #440

Merged
merged 12 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ccm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
branches:
- main
- '[0-9][0-9][0-9][0-9].[0-9][0-9].*' # 2021.01.x
- '2024-03-01-dep-update'
- 'i330_alt_cjs_esm_upgrade'
tags:
- '[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]' # 2021.01.01

Expand All @@ -21,7 +23,7 @@ env:
jobs:
build:
# to test a feature, change the repo name to your github id
if: github.repository_owner == 'tl-its-umich-edu'
if: github.repository_owner == 'tl-its-umich-edu' || github.repository_owner == 'pushyamig'
runs-on: ubuntu-latest
steps:

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ you create a migration file and run the migration using `umzug`.
Developers have to write `up` and `down` migration steps manually.

1. Running migrations locally
1. Run migrations: `docker exec -it ccm_web node -r ts-node/register server/src/migrator up`
2. Revert a migration: `docker exec -it ccm_web node -r ts-node/register server/src/migrator down`.
3. Create a migration file: `docker exec -it ccm_web node -r ts-node/register server/src/migrator create --name my-migration.ts`.
1. Run migrations: `docker exec -it ccm_web node --loader ts-node/esm server/src/migrator.ts up`
2. Revert a migration: `docker exec -it ccm_web node --loader ts-node/esm server/src/migrator.ts down`.
3. Create a migration file: `docker exec -it ccm_web node --loader ts-node/esm server/src/migrator.ts create --name my-migration.ts`.

This generates a migration file called `<timestamp>.my-migration.ts`.
The timestamp prefix can be customized to be date-only or omitted,
Expand All @@ -201,9 +201,9 @@ Developers have to write `up` and `down` migration steps manually.
2. Running the migration are usually done when server is starting up, but in addition if you want to run migrations or revert use above commands

3. Running migrations `docker-compose-prod.yml`
1. For running the migrations in in dev/test/prod, use `docker exec -it ccm_web_prod node server/src/migrator up` and `docker exec -it ccm_web_prod node server/src/migrator down`.
1. For running the migrations in in dev/test/prod, use `docker exec -it ccm_web_prod node --loader ts-node/esm server/src/migrator.js up` and `docker exec -it ccm_web_prod node --loader ts-node/esm server/src/migrator.js down`.
2. The reason for the separate setups for running migrations for local/non-prod and prod is locally, we don't
transpile TypeScript to Javascript and so we always use `ts-node/register` module for running in node
transpile TypeScript to Javascript and so we always use `ts-node/esm` module for running in node
environment.

#### Troubleshooting
Expand Down Expand Up @@ -352,7 +352,7 @@ This code will hopefully only remain in this repository temporarily.
2. The action is triggered whenever a commit is made to the `main` branch. E.g., when a pull request is merged to `main`.
3. OpenShift projects can periodically pull this image from GHCR. Configure only **_NON-PRODUCTION_** CCM projects to pull the image…
```sh
oc tag ghcr.io/tl-its-umich-edu/canvas-course-manager-next:latest canvas-course-manager-next:latest --scheduled --reference-policy=local
oc tag ghcr.io/tl-its-umich-edu/canvas-course-manager-next:main canvas-course-manager-next:main --scheduled --reference-policy=local
```
See the OpenShift documentation "[Managing image streams: Configuring periodic importing of image stream tags](https://docs.openshift.com/container-platform/4.11/openshift_images/image-streams-manage.html#images-imagestream-import_image-streams-managing)" for details.

Expand Down
10 changes: 5 additions & 5 deletions ccm_web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# - https://dev.to/chrsgrrtt/dockerising-a-next-js-project-1ck5

# Base stage
FROM node:16-slim AS base
FROM node:20-slim AS base
WORKDIR /base/

COPY package*.json ./
Expand All @@ -14,25 +14,25 @@ ARG PORT
EXPOSE ${PORT}

# Build stage (build client and compile server to JS)
FROM node:16-slim AS build
FROM node:20-slim AS build
WORKDIR /build/

COPY --from=base /base ./
RUN npm run build

# Prod stage
FROM node:16-slim AS prod
FROM node:20-slim AS prod
ENV NODE_ENV=production
WORKDIR /app

COPY --from=base \
/base/package.json \
/base/package-lock.json \
/base/start.sh \
/base/ecosystem.config.js \
/base/ecosystem.config.cjs \
./
RUN npm install --production
RUN npm install pm2@5.2.0 -g
RUN npm install pm2@5.3.1 -g
COPY --from=build /build/dist/ ./

# Set PM2_HOME so that .pm2 files are written in /tmp/
Expand Down
49 changes: 25 additions & 24 deletions ccm_web/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import React, { useEffect, useState } from 'react'
import { Route, Switch, useLocation } from 'react-router-dom'
import { Route, Routes, useLocation } from 'react-router-dom'

import { getCourse } from './api'
import { getCourse } from './api.js'
import './App.css'
import APIErrorMessage from './components/APIErrorMessage'
import AuthorizePrompt from './components/AuthorizePrompt'
import ErrorAlert from './components/ErrorAlert'
import Layout from './components/Layout'
import useGlobals from './hooks/useGlobals'
import usePromise from './hooks/usePromise'
import { CanvasCourseBase } from './models/canvas'
import allFeatures from './models/FeatureUIData'
import Home from './pages/Home'
import NotFound from './pages/NotFound'
import redirect from './utils/redirect'
import APIErrorMessage from './components/APIErrorMessage.js'
import AuthorizePrompt from './components/AuthorizePrompt.js'
import ErrorAlert from './components/ErrorAlert.js'
import Layout from './components/Layout.js'
import useGlobals from './hooks/useGlobals.js'
import usePromise from './hooks/usePromise.js'
import { CanvasCourseBase } from './models/canvas.js'
import allFeatures from './models/FeatureUIData.js'
import Home from './pages/Home.js'
import NotFound from './pages/NotFound.js'
import redirect from './utils/redirect.js'

function App (): JSX.Element {
const features = allFeatures.map(f => f.features).flat()

const location = useLocation()

const [globals, isAuthenticated, isLoading, globalsError, csrfTokenCookieError] = useGlobals()
const [globals, csrfToken, isAuthenticated, isLoading, globalsError, csrfTokenCookieError] = useGlobals()

const [course, setCourse] = useState<undefined|CanvasCourseBase>(undefined)
const [doLoadCourse, isCourseLoading, getCourseError] = usePromise<CanvasCourseBase|undefined, typeof getCourse>(
Expand All @@ -42,7 +42,7 @@ function App (): JSX.Element {

if (globalsError !== undefined) console.error(globalsError)
if (csrfTokenCookieError !== undefined) console.error(csrfTokenCookieError)
if (globals === undefined || !isAuthenticated) {
if (globals === undefined || !isAuthenticated || csrfToken === undefined) {
redirect('/access-denied')
return (loading)
}
Expand Down Expand Up @@ -71,25 +71,26 @@ function App (): JSX.Element {
: undefined

return (
<Layout {...{ features, pathnames }} devMode={globals?.environment === 'development'}>
<Switch>
<Route exact={true} path='/'>
<Home globals={globals} course={course} setCourse={setCourse} getCourseError={getCourseError} />
</Route>
<Layout {...{ features, pathnames }} devMode={globals?.environment === 'development'} csrfToken={csrfToken}>
<Routes>
<Route path='/' element={
<Home globals={globals} csrfToken={csrfToken} course={course} setCourse={setCourse} getCourseError={getCourseError} />
} />
{features.map(feature => {
return (
<Route key={feature.data.id} path={feature.route}>
<Route key={feature.data.id} path={feature.route} element={
<feature.component
globals={globals}
csrfToken={csrfToken}
course={course}
title={feature.data.title}
helpURLEnding={feature.data.helpURLEnding}
/>
</Route>
}/>
)
})}
<Route><NotFound /></Route>
</Switch>
<Route path="/*" element={<NotFound />}/>
</Routes>
</Layout>
)
}
Expand Down
62 changes: 28 additions & 34 deletions ccm_web/client/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import Cookies from 'js-cookie'
import {
CanvasCourseBase, CanvasCourseSection, CanvasCourseSectionBase, CanvasEnrollment,
CanvasUserCondensed, CourseWithSections
} from './models/canvas'
import { ExternalUserSuccess } from './models/externalUser'
import { Globals } from './models/models'
import handleErrors, { CanvasError } from './utils/handleErrors'
} from './models/canvas.js'
import { ExternalUserSuccess } from './models/externalUser.js'
import { Globals, CsrfToken } from './models/models.js'
import handleErrors, { CanvasError } from './utils/handleErrors.js'

const jsonMimeType = 'application/json'

export const getCSRFToken = (): string | undefined => Cookies.get('CSRF-Token')

const initCSRFRequest = (headers: string[][]): RequestInit => {
const csrfToken = getCSRFToken()
if (csrfToken !== undefined) headers.push(['CSRF-Token', csrfToken])
const addStateChangeCallHeaders = (csrfToken: string): RequestInit => {
const headers: Array<[string, string]> = [['Content-Type', jsonMimeType], ['Accept', jsonMimeType], ['x-csrf-token', csrfToken]]
const request: RequestInit = { headers }
return request
}
Expand All @@ -24,26 +20,23 @@ const getGet = (): RequestInit => {
return request
}

const getPost = (body: string): RequestInit => {
const headers: string[][] = [['Content-Type', jsonMimeType], ['Accept', jsonMimeType]]
const request = initCSRFRequest(headers)
const getPost = (body: string, csrfToken: string): RequestInit => {
const request = addStateChangeCallHeaders(csrfToken)
request.method = 'POST'
request.body = body
return request
}

const getDelete = (body: string): RequestInit => {
const headers: string[][] = [['Content-Type', jsonMimeType], ['Accept', jsonMimeType]]
const request = initCSRFRequest(headers)
const getDelete = (body: string, csrfToken: string): RequestInit => {
const request = addStateChangeCallHeaders(csrfToken)
request.method = 'DELETE'
request.body = body
return request
}

// This currently assumes all put requests have a JSON payload and receive a JSON response.
const getPut = (body: string): RequestInit => {
const headers: string[][] = [['Content-Type', jsonMimeType], ['Accept', jsonMimeType]]
const request = initCSRFRequest(headers)
const getPut = (body: string, csrfToken: string): RequestInit => {
const request = addStateChangeCallHeaders(csrfToken)
request.method = 'PUT'
request.body = body
return request
Expand All @@ -56,8 +49,8 @@ export const getCourse = async (courseId: number): Promise<CanvasCourseBase> =>
return await resp.json()
}

export const setCourseName = async (courseId: number, newName: string): Promise<CanvasCourseBase> => {
const request = getPut(JSON.stringify({ newName: newName }))
export const setCourseName = async (courseId: number, newName: string, csrfToken: string): Promise<CanvasCourseBase> => {
const request = getPut(JSON.stringify({ newName: newName }), csrfToken)
const resp = await fetch(`/api/course/${courseId}/name`, request)
await handleErrors(resp)
return await resp.json()
Expand All @@ -77,9 +70,9 @@ export const getCourseSections = async (courseId: number): Promise<CanvasCourseS
return await resp.json()
}

export const addCourseSections = async (courseId: number, sectionNames: string[]): Promise<CanvasCourseSection[]> => {
export const addCourseSections = async (courseId: number, sectionNames: string[], csrfToken: string): Promise<CanvasCourseSection[]> => {
const body = JSON.stringify({ sections: sectionNames })
const request = getPost(body)
const request = getPost(body, csrfToken)
const resp = await fetch('/api/course/' + courseId.toString() + '/sections', request)
await handleErrors(resp)
return await resp.json()
Expand All @@ -102,27 +95,28 @@ export const getStudentsEnrolledInSection = async (sectionId: number): Promise<s
}

export const addSectionEnrollments = async (
sectionId: number, enrollments: AddSectionEnrollment[]
sectionId: number, enrollments: AddSectionEnrollment[], csrfToken: string
): Promise<CanvasEnrollment[]> => {
const body = JSON.stringify({ users: enrollments })
const request = getPost(body)
const request = getPost(body, csrfToken)
const resp = await fetch(`/api/sections/${sectionId}/enroll`, request)
await handleErrors(resp)
return await resp.json()
}

export const addEnrollmentsToSections = async (enrollments: AddEnrollmentWithSectionId[]): Promise<CanvasEnrollment[]> => {
export const addEnrollmentsToSections = async (enrollments: AddEnrollmentWithSectionId[], csrfToken: string): Promise<CanvasEnrollment[]> => {
const body = JSON.stringify({ enrollments })
const request = getPost(body)
const request = getPost(body, csrfToken)
const resp = await fetch('/api/sections/enroll', request)
await handleErrors(resp)
return await resp.json()
}

export const setCSRFTokenCookie = async (): Promise<void> => {
export const getCSRFTokenResponse = async (): Promise<CsrfToken> => {
const request = getGet()
const resp = await fetch('/auth/csrfToken', request)
await handleErrors(resp)
return await resp.json()
}

export const getTeacherSections = async (termId: number): Promise<CourseWithSections[]> => {
Expand All @@ -140,17 +134,17 @@ export const searchSections = async (termId: number, searchType: 'uniqname' | 'c
return await resp.json()
}

export const mergeSections = async (courseId: number, sectionsToMerge: CanvasCourseSection[]): Promise<CanvasCourseSectionBase[]> => {
export const mergeSections = async (courseId: number, sectionsToMerge: CanvasCourseSection[], csrfToken: string): Promise<CanvasCourseSectionBase[]> => {
const body = JSON.stringify({ sectionIds: sectionsToMerge.map(section => { return section.id }) })
const request = getPost(body)
const request = getPost(body, csrfToken)
const resp = await fetch(`/api/course/${courseId}/sections/merge`, request)
await handleErrors(resp)
return await resp.json()
}

export const unmergeSections = async (sectionsToUnmerge: CanvasCourseSection[]): Promise<CanvasCourseSectionBase[]> => {
export const unmergeSections = async (sectionsToUnmerge: CanvasCourseSection[], csrfToken: string): Promise<CanvasCourseSectionBase[]> => {
const body = JSON.stringify({ sectionIds: sectionsToUnmerge.map(section => { return section.id }) })
const request = getDelete(body)
const request = getDelete(body, csrfToken)
const resp = await fetch('/api/sections/unmerge', request)
await handleErrors(resp)
return await resp.json()
Expand Down Expand Up @@ -181,9 +175,9 @@ interface ExternalUser {
givenName: string
}

export const createExternalUsers = async (newUsers: ExternalUser[]): Promise<ExternalUserSuccess[]> => {
export const createExternalUsers = async (newUsers: ExternalUser[], csrfToken: string): Promise<ExternalUserSuccess[]> => {
const body = JSON.stringify({ users: newUsers })
const request = getPost(body)
const request = getPost(body, csrfToken)
const resp = await fetch('/api/admin/createExternalUsers', request)
await handleErrors(resp)
return await resp.json()
Expand Down
4 changes: 2 additions & 2 deletions ccm_web/client/src/components/APIErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { Typography } from '@material-ui/core'
import { Typography } from '@mui/material'

import { CanvasError } from '../utils/handleErrors'
import { CanvasError } from '../utils/handleErrors.js'

interface APIErrorMessageProps {
context: string
Expand Down
4 changes: 2 additions & 2 deletions ccm_web/client/src/components/APIErrorsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from 'react'

import CustomTable, { TableColumn } from './CustomTable'
import { ErrorDescription } from '../utils/handleErrors'
import CustomTable, { TableColumn } from './CustomTable.js'
import { ErrorDescription } from '../utils/handleErrors.js'

interface NumberedErrorDescription extends ErrorDescription {
rowNumber: number
Expand Down
Loading