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

check for unique ID error during Canvas user creation #7

Merged
merged 6 commits into from
Feb 24, 2022
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
6 changes: 4 additions & 2 deletions ccm_web/server/src/api/api.admin.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import CanvasRequestor from '@kth/canvas-api'
import { CourseApiHandler } from './api.course.handler'
import { APIErrorData, isAPIErrorData } from './api.interfaces'
import {
checkForUniqueIdError,
createLimitedPromises,
handleAPIError,
HttpMethod,
Expand Down Expand Up @@ -119,7 +120,7 @@ export class AdminApiHandler {
return finalResult
}

async createExternalUser (user: ExternalUserDto, accountID: number): Promise<CanvasUserLoginEmail | APIErrorData> {
async createExternalUser (user: ExternalUserDto, accountID: number): Promise<CanvasUserLoginEmail | APIErrorData | false> {
const email = user.email
const loginId = email.replace('@', '+')
const fullName = `${user.givenName} ${user.surname}`
Expand Down Expand Up @@ -158,6 +159,7 @@ export class AdminApiHandler {
} = response.body
return { id, name, sortable_name, short_name, login_id, email }
} catch (error: unknown) {
if (checkForUniqueIdError(error)) return false
const errorResponse = handleAPIError(error, loginId)
return {
statusCode: errorResponse.canvasStatusCode,
Expand All @@ -168,7 +170,7 @@ export class AdminApiHandler {

async createExternalUsers (
users: ExternalUserDto[], accountID: number
): Promise<Array<{ result: CanvasUserLoginEmail | APIErrorData, email: string }>> {
): Promise<Array<{ result: CanvasUserLoginEmail | APIErrorData | false, email: string }>> {
const start = process.hrtime.bigint()

// Try creating all Canvas users; failure often means user already exists
Expand Down
15 changes: 4 additions & 11 deletions ccm_web/server/src/api/api.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SessionData } from 'express-session'
import { HttpStatus, Injectable } from '@nestjs/common'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'

import { AdminApiHandler } from './api.admin.handler'
Expand Down Expand Up @@ -146,19 +146,12 @@ export class APIService {

// Handle create user responses: success, failure, and already exists
createUserResponses.forEach(({ email, result }) => {
let userCreated: false | APIErrorData | CanvasUserLoginEmail
if (isAPIErrorData(result)) {
if (result.statusCode === HttpStatus.BAD_REQUEST) {
userCreated = false
} else {
userCreated = result
createErrors.push(result)
}
} else {
userCreated = result
createErrors.push(result)
} else if (result !== false) {
newUsers.push(result)
}
resultData[email] = { userCreated }
resultData[email] = { userCreated: result }
})
if (createErrors.length === externalUsers.length) {
const statusCode = determineStatusCode(createErrors.map(e => e.statusCode))
Expand Down
49 changes: 32 additions & 17 deletions ccm_web/server/src/api/api.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import pLimit from 'p-limit'
import {
APIErrorData, APIErrorPayload, isAPIErrorData
} from './api.interfaces'
import { isCanvasErrorBody } from '../canvas/canvas.interfaces'
import { isCanvasMessageErrorBody, isCanvasMessageErrorsBody, isCanvasUniqueIdErrorsBody } from '../canvas/canvas.interfaces'

import baseLogger from '../logger'

Expand All @@ -20,13 +20,43 @@ export enum HttpMethod {
Delete = 'DELETE'
}

export function checkForUniqueIdError (error: unknown): boolean {
lsloan marked this conversation as resolved.
Show resolved Hide resolved
if (!(error instanceof CanvasApiError && error.response !== undefined)) return false
const { statusCode, body } = error.response
logger.debug('Checking if an error was thrown because the user already exists...')
logger.debug(JSON.stringify(body, null, 2))
return (
statusCode === HttpStatus.BAD_REQUEST &&
isCanvasUniqueIdErrorsBody(body) &&
body.errors.pseudonym.unique_id.length > 0 &&
body.errors.pseudonym.unique_id[0].type === 'taken'
)
}

function parseErrorBody (body: unknown): string {
if (body === null || body === undefined || String(body).startsWith('<!DOCTYPE html>')) return 'No response body was found.'
if (isCanvasMessageErrorBody(body)) {
return body.message
} else if (isCanvasMessageErrorsBody(body)) {
return body.errors.map(e => e.message).join(' ')
} else if (isCanvasUniqueIdErrorsBody(body)) {
return (
body.errors.pseudonym.unique_id.length > 0
? body.errors.pseudonym.unique_id[0].message
: 'Unique ID error had no message.'
)
} else {
return `Canvas response body had unhandled shape: ${JSON.stringify(body)}`
}
}

export function handleAPIError (error: unknown, input?: string): APIErrorPayload {
const failedInput = input === undefined ? null : input
if (error instanceof CanvasApiError && error.response !== undefined) {
const { statusCode, body } = error.response
const bodyText = parseErrorBody(body)
logger.error(`Received error status code: (${String(statusCode)})`)
logger.error(`Response body: (${bodyText})`)
logger.error(`Response message(s): (${bodyText})`)
logger.error(`Failed input: (${String(failedInput)})`)
return { canvasStatusCode: statusCode, message: bodyText, failedInput: failedInput }
} else {
Expand All @@ -36,21 +66,6 @@ export function handleAPIError (error: unknown, input?: string): APIErrorPayload
}
}

export function parseErrorBody (body: unknown): string {
if (body === null || body === undefined || String(body).startsWith('<!DOCTYPE html>')) return 'No response body was found.'
if (!isCanvasErrorBody(body)) {
return `Response body had unexpected shape: ${JSON.stringify(body)}`
}
let errorMessage: string
try {
errorMessage = body.errors.map(e => e.message).join(' ')
} catch (e) {
errorMessage = JSON.stringify(body)
logger.debug(errorMessage)
}
return errorMessage
}

export function determineStatusCode (codes: HttpStatus[]): HttpStatus {
if (codes.length === 0) throw new Error('determineStatusCode received an array with length 0.')
const uniqueStatusCodes: Set<HttpStatus> = new Set(codes)
Expand Down
60 changes: 46 additions & 14 deletions ccm_web/server/src/canvas/canvas.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,33 +142,65 @@ export interface CourseWithSections extends CanvasCourseBase {
sections: CanvasCourseSection[]
}

// Errors
// Error Data

interface CanvasError {
export interface CanvasMessageErrorBody {
message: string
}

function isCanvasError (value: unknown): value is CanvasError {
export function isCanvasMessageErrorBody (value: unknown): value is CanvasMessageErrorBody {
return hasKeys(value, ['message'])
}

export interface CanvasErrorBody {
errors: CanvasError[]
export interface CanvasErrorsBody {
errors: unknown
}

export function isCanvasErrorBody (value: unknown): value is CanvasErrorBody {
if (!hasKeys(value, ['errors'])) {
return false
}
export function isCanvasErrorsBody (value: unknown): value is CanvasErrorsBody {
return hasKeys(value, ['errors'])
}

export interface CanvasMessageErrorsBody extends CanvasErrorsBody {
errors: CanvasMessageErrorBody[]
}

export function isCanvasMessageErrorsBody (value: unknown): value is CanvasMessageErrorsBody {
if (!isCanvasErrorsBody(value)) return false
return (
Array.isArray(value.errors) &&
value.errors.every(e => isCanvasMessageErrorBody(e))
)
}

if (Array.isArray(value.errors)) {
const result = value.errors.map(e => isCanvasError(e)).every(e => e)
return result
} else {
return true
interface UniqueIdErrorData {
attribute: 'unique_id'
type: string
message: string
}

export interface CanvasUniqueIdErrorsBody extends CanvasErrorsBody {
errors: {
pseudonym: {
unique_id: UniqueIdErrorData[]
}
}
}

export function isCanvasUniqueIdErrorsBody (value: unknown): value is CanvasUniqueIdErrorsBody {
if (!isCanvasErrorsBody(value)) return false
return (
hasKeys(value.errors, ['pseudonym']) &&
hasKeys(value.errors.pseudonym, ['unique_id']) &&
Array.isArray(value.errors.pseudonym.unique_id) &&
value.errors.pseudonym.unique_id.every(o => {
return (
hasKeys(o, ['attribute', 'type', 'message']) &&
Object.values(o).every(v => typeof v === 'string')
)
})
)
}
lsloan marked this conversation as resolved.
Show resolved Hide resolved

export const isOAuthErrorResponseQuery = (value: unknown): value is OAuthErrorResponseQuery => {
return hasKeys(value, ['error'])
}