From 162c825d807c409bab8a8c7671af41b695b93e0a Mon Sep 17 00:00:00 2001 From: Brooke Date: Fri, 23 Dec 2022 11:27:23 -0800 Subject: [PATCH 1/6] TCA-852 #comment This commit prevents wipro users w/out dice enabled from access any TCA lesson. #time 1h --- .../token-functions/token.functions.ts | 6 ++-- .../functions/token-functions/token.model.ts | 1 + src-ts/lib/functions/user-functions/index.ts | 5 +++- .../user-functions/user-store/index.ts | 5 +++- .../user-store/user-xhr.store.ts | 17 ++++++++++- .../user-functions/user.functions.ts | 8 +++-- src-ts/lib/page-footer/PageFooter.tsx | 18 ++++++----- .../profile-factory/profile.factory.ts | 14 ++++++--- .../profile-functions/profile.functions.ts | 14 +++++---- .../profile-provider/user-profile.model.ts | 2 ++ .../course-curriculum/CourseCurriculum.tsx | 17 +++++++++-- .../learn/free-code-camp/FreeCodeCamp.tsx | 30 +++++++++++++++---- 12 files changed, 106 insertions(+), 31 deletions(-) diff --git a/src-ts/lib/functions/token-functions/token.functions.ts b/src-ts/lib/functions/token-functions/token.functions.ts index 32b523196..3e8059fe9 100644 --- a/src-ts/lib/functions/token-functions/token.functions.ts +++ b/src-ts/lib/functions/token-functions/token.functions.ts @@ -15,9 +15,10 @@ export async function getAsync(): Promise { } try { - const { handle, roles }: { + const { handle, roles, userId }: { handle?: string roles?: Array + userId?: number } = decodeToken(token) // if we didn't find the handle, we have a bad token @@ -26,8 +27,9 @@ export async function getAsync(): Promise { return Promise.resolve({}) } - return Promise.resolve({ handle, roles, token }) + return Promise.resolve({ handle, roles, token, userId }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { logError(error) return Promise.resolve({}) diff --git a/src-ts/lib/functions/token-functions/token.model.ts b/src-ts/lib/functions/token-functions/token.model.ts index 9a5c03bec..dd4b8e1b9 100644 --- a/src-ts/lib/functions/token-functions/token.model.ts +++ b/src-ts/lib/functions/token-functions/token.model.ts @@ -2,4 +2,5 @@ export interface TokenModel { handle?: string roles?: Array token?: string + userId?: number } diff --git a/src-ts/lib/functions/user-functions/index.ts b/src-ts/lib/functions/user-functions/index.ts index 617ec8e4a..7a5ad1a47 100644 --- a/src-ts/lib/functions/user-functions/index.ts +++ b/src-ts/lib/functions/user-functions/index.ts @@ -1 +1,4 @@ -export { updatePasswordAsync as userUpdatePasswordAsync } from './user.functions' +export { + getDiceStatusAsync as userGetDiceStatusAsync, + updatePasswordAsync as userUpdatePasswordAsync, +} from './user.functions' diff --git a/src-ts/lib/functions/user-functions/user-store/index.ts b/src-ts/lib/functions/user-functions/user-store/index.ts index fa880fb95..368784135 100644 --- a/src-ts/lib/functions/user-functions/user-store/index.ts +++ b/src-ts/lib/functions/user-functions/user-store/index.ts @@ -1,2 +1,5 @@ -export { patchAsync as userPatchAsync } from './user-xhr.store' +export { + getDiceStatusAsync as userStoreGetDiceStatusAsync, + patchAsync as userStorePatchAsync, +} from './user-xhr.store' export { type UserPatchRequest } from './user-xhr.store' diff --git a/src-ts/lib/functions/user-functions/user-store/user-xhr.store.ts b/src-ts/lib/functions/user-functions/user-store/user-xhr.store.ts index 789e41458..faf3951b2 100644 --- a/src-ts/lib/functions/user-functions/user-store/user-xhr.store.ts +++ b/src-ts/lib/functions/user-functions/user-store/user-xhr.store.ts @@ -1,5 +1,5 @@ import { User } from '../../../../../types/tc-auth-lib' -import { xhrPatchAsync } from '../../xhr-functions' +import { xhrGetAsync, xhrPatchAsync } from '../../xhr-functions' import { user as userEndpoint } from './user-endpoint.config' @@ -12,6 +12,21 @@ export interface UserPatchRequest { } } +export async function getDiceStatusAsync(userId: number): Promise { + + interface DiceStatusResult { + result: { + content: { + diceEnabled: boolean + } + } + } + const result: DiceStatusResult + = await xhrGetAsync(`${userEndpoint(userId)}/2fa`) + + return !!result.result.content.diceEnabled +} + export async function patchAsync(userId: number, request: UserPatchRequest): Promise { const url: string = userEndpoint(userId) return xhrPatchAsync(url, request) diff --git a/src-ts/lib/functions/user-functions/user.functions.ts b/src-ts/lib/functions/user-functions/user.functions.ts index 792d33791..d240c8d5b 100644 --- a/src-ts/lib/functions/user-functions/user.functions.ts +++ b/src-ts/lib/functions/user-functions/user.functions.ts @@ -1,4 +1,8 @@ -import { userPatchAsync, UserPatchRequest } from './user-store' +import { UserPatchRequest, userStoreGetDiceStatusAsync, userStorePatchAsync } from './user-store' + +export async function getDiceStatusAsync(userId: number): Promise { + return userStoreGetDiceStatusAsync(userId) +} export async function updatePasswordAsync(userId: number, currentPassword: string, password: string): Promise { const request: UserPatchRequest = { @@ -9,6 +13,6 @@ export async function updatePasswordAsync(userId: number, currentPassword: strin }, }, } - return userPatchAsync(userId, request) + return userStorePatchAsync(userId, request) .then(() => undefined) } diff --git a/src-ts/lib/page-footer/PageFooter.tsx b/src-ts/lib/page-footer/PageFooter.tsx index 1762265a8..9b3bdfbfa 100644 --- a/src-ts/lib/page-footer/PageFooter.tsx +++ b/src-ts/lib/page-footer/PageFooter.tsx @@ -7,13 +7,17 @@ const PageFooter: FC<{}> = () => { const navElementId: string = 'footer-nav-el' - tcUniNav( - 'init', - navElementId, - { - type: 'footer', - }, - ) + // delay the initialization so + // the nav element has time to render + setTimeout(() => { + tcUniNav( + 'init', + navElementId, + { + type: 'footer', + }, + ) + }, 10) return
} diff --git a/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts b/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts index 7e5fb0aff..ff2f565c3 100644 --- a/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts +++ b/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts @@ -3,18 +3,24 @@ import { UserProfile } from '../../user-profile.model' import { UserRole } from './user-role.enum' -export function create(profile: UserProfile, token: TokenModel): UserProfile { - // TODO: create the profile full name property +export function create(profile: UserProfile, token: TokenModel, hasMfaEnabled: boolean): UserProfile { + // Currently, the "Self-Service Customer" role is being set when a user is created // during the self-service workflow. There are no other roles being set to distinguish // between Customers and Members. // Therefore, the only way to know if a user is a Member is if s/he is not a Customer. // This is imperfect, bc a user could be both a Customer or a Member, but for now // we are okay with this and will have a more in-depth initiave to properly assign - // rolees. + // roles. profile.isCustomer = !!token.roles?.some(role => role === UserRole.customer) profile.isMember = !profile.isCustomer + + profile.isWipro = profile.email.endsWith('@topcoder.com') + profile.diceEnabled = hasMfaEnabled + // store roles for custom capability checks - profile.roles = token.roles + profile.roles = token.roles || [] + + // TODO: create the profile full name property return profile } diff --git a/src-ts/lib/profile-provider/profile-functions/profile.functions.ts b/src-ts/lib/profile-provider/profile-functions/profile.functions.ts index 2499043a6..d73404bdf 100644 --- a/src-ts/lib/profile-provider/profile-functions/profile.functions.ts +++ b/src-ts/lib/profile-provider/profile-functions/profile.functions.ts @@ -1,3 +1,4 @@ +import { userGetDiceStatusAsync } from '../../functions/user-functions' import { tokenGetAsync, TokenModel } from '../../functions/token-functions' import { EditNameRequest } from '../edit-name-request.model' import { UserProfile } from '../user-profile.model' @@ -11,19 +12,22 @@ export async function getAsync(handle?: string): Promise = profileStoreGet(safeHandle) + const mfaPromise: Promise = userGetDiceStatusAsync(token.userId) + + const [profileResult, mfaEnabled]: [UserProfile, boolean] = await Promise.all([profilePromise, mfaPromise]) // make the changes we need based on the token - const output: UserProfile = profileFactoryCreate(profileResult, token) + const output: UserProfile = profileFactoryCreate(profileResult, token, mfaEnabled) return output } -export async function editNameAsync(handle: string, profile: EditNameRequest): Promise { +export async function editNameAsync(handle: string, profile: EditNameRequest): Promise { return profileStorePatchName(handle, profile) } diff --git a/src-ts/lib/profile-provider/user-profile.model.ts b/src-ts/lib/profile-provider/user-profile.model.ts index 51ae0c2e1..d557c5e22 100644 --- a/src-ts/lib/profile-provider/user-profile.model.ts +++ b/src-ts/lib/profile-provider/user-profile.model.ts @@ -1,6 +1,7 @@ export interface UserProfile { competitionCountryCode: string createdAt: number + diceEnabled: boolean email: string firstName: string handle: string @@ -8,6 +9,7 @@ export interface UserProfile { homeCountryCode: string isCustomer?: boolean isMember?: boolean + isWipro: boolean lastName: string photoURL?: string roles: Array diff --git a/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx b/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx index 408c65466..51637a968 100644 --- a/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx +++ b/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx @@ -37,11 +37,12 @@ interface CourseCurriculumProps { const CourseCurriculum: FC = (props: CourseCurriculumProps) => { const navigate: NavigateFunction = useNavigate() - const [searchParams]: any = useSearchParams() + const [searchParams]: [URLSearchParams, unknown] = useSearchParams() const isLoggedIn: boolean = !!props.profile - const [isTcAcademyPolicyModal, setIsTcAcademyPolicyModal]: [boolean, Dispatch>] = useState(false) + const [isTcAcademyPolicyModal, setIsTcAcademyPolicyModal]: [boolean, Dispatch>] + = useState(false) const status: string = props.progress?.status ?? UserCertificationProgressStatus.inititialized const completedPercentage: number = (props.progress?.courseProgressPercentage ?? 0) / 100 @@ -76,6 +77,7 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp * Handle user click on start course/resume/login button */ const handleStartCourseClick: () => void = useCallback(() => { + console.debug(props.profile) // if user is not logged in, redirect to login page if (!isLoggedIn) { // add a flag to the return url to show the academic policy modal @@ -84,6 +86,14 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp return } + // if the user is wipro and s/he hasn't set up DICE, + // let them know + if (props.profile?.isWipro && !props.profile.diceEnabled) { + // TODO + console.debug('user needs dice') + return + } + // Check if user accepted policy and resume(or start) the course if (props.progress?.academicHonestyPolicyAcceptedAt) { handleStartCourse() @@ -92,6 +102,7 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp // show the academic policy modal before starting a new course setIsTcAcademyPolicyModal(true) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ handleStartCourse, isLoggedIn, @@ -130,6 +141,7 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp } handleStartCourse() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ handleStartCourse, props.course.certificationId, @@ -149,6 +161,7 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp * proceed as if the user just clicked "Start course" button */ useEffect(() => { + // eslint-disable-next-line no-null/no-null if (props.progressReady && isLoggedIn && searchParams.get(LEARN_PATHS.startCourseRouteFlag) !== null) { handleStartCourseClick() } diff --git a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx index c76e6e532..725bfe76a 100644 --- a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx +++ b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx @@ -436,19 +436,37 @@ const FreeCodeCamp: FC<{}> = () => { /** * Check if the user accepted the academic honesty policy + * and either is not a wipro user or the wipro user has dice enabled. * if not, redirect user to course details page to accept the policy */ useLayoutEffect(() => { - if (ready && !(isLoggedIn && certificateProgress?.academicHonestyPolicyAcceptedAt)) { - const coursePath: string = getCoursePath( - providerParam, - certificationParam, - ) - navigate(coursePath) + + // if we're not ready, there's nothing to do + if (!ready) { + return } + + // if the user is logged in, + // and the user is a either not wipro user or is a wipro user with dice enabled, + // and if the user has accepted the academic honesty policy, + // the user is permitted to take the course, so there's nothing to do. + if (isLoggedIn + && (!profile?.isWipro || !!profile?.diceEnabled) + && !!certificateProgress?.academicHonestyPolicyAcceptedAt) { + return + } + + // redirect the user to course details page to perform the + // necessary actions + const coursePath: string = getCoursePath( + providerParam, + certificationParam, + ) + navigate(coursePath) }, [ ready, certificateProgress, + profile, providerParam, certificationParam, navigate, From 91e0bdff6c460e5f566b4b2253e201e14894c229 Mon Sep 17 00:00:00 2001 From: Brooke Date: Fri, 23 Dec 2022 11:27:43 -0800 Subject: [PATCH 2/6] TCA-852 Change domain back to wipro --- .../profile-functions/profile-factory/profile.factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts b/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts index ff2f565c3..6567dafae 100644 --- a/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts +++ b/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts @@ -15,7 +15,7 @@ export function create(profile: UserProfile, token: TokenModel, hasMfaEnabled: b profile.isCustomer = !!token.roles?.some(role => role === UserRole.customer) profile.isMember = !profile.isCustomer - profile.isWipro = profile.email.endsWith('@topcoder.com') + profile.isWipro = profile.email.endsWith('@wipro.com') profile.diceEnabled = hasMfaEnabled // store roles for custom capability checks From 5f71fc51d8c2fd59671e9f28cd50a900fbb82f9b Mon Sep 17 00:00:00 2001 From: Brooke Date: Fri, 23 Dec 2022 11:38:34 -0800 Subject: [PATCH 3/6] TCA-852 Fix argument name --- .../profile-functions/profile-factory/profile.factory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts b/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts index 6567dafae..13fe53fdd 100644 --- a/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts +++ b/src-ts/lib/profile-provider/profile-functions/profile-factory/profile.factory.ts @@ -3,7 +3,7 @@ import { UserProfile } from '../../user-profile.model' import { UserRole } from './user-role.enum' -export function create(profile: UserProfile, token: TokenModel, hasMfaEnabled: boolean): UserProfile { +export function create(profile: UserProfile, token: TokenModel, hasDiceEnabled: boolean): UserProfile { // Currently, the "Self-Service Customer" role is being set when a user is created // during the self-service workflow. There are no other roles being set to distinguish @@ -16,7 +16,7 @@ export function create(profile: UserProfile, token: TokenModel, hasMfaEnabled: b profile.isMember = !profile.isCustomer profile.isWipro = profile.email.endsWith('@wipro.com') - profile.diceEnabled = hasMfaEnabled + profile.diceEnabled = hasDiceEnabled // store roles for custom capability checks profile.roles = token.roles || [] From 0a15807c4e929bb72e62a18ad81ec14cf7ace7a0 Mon Sep 17 00:00:00 2001 From: Brooke Date: Fri, 23 Dec 2022 11:40:10 -0800 Subject: [PATCH 4/6] TCA-852 fix property names --- .../profile-provider/profile-functions/profile.functions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-ts/lib/profile-provider/profile-functions/profile.functions.ts b/src-ts/lib/profile-provider/profile-functions/profile.functions.ts index d73404bdf..ede0f9ef7 100644 --- a/src-ts/lib/profile-provider/profile-functions/profile.functions.ts +++ b/src-ts/lib/profile-provider/profile-functions/profile.functions.ts @@ -19,12 +19,12 @@ export async function getAsync(handle?: string): Promise = profileStoreGet(safeHandle) - const mfaPromise: Promise = userGetDiceStatusAsync(token.userId) + const dicePromise: Promise = userGetDiceStatusAsync(token.userId) - const [profileResult, mfaEnabled]: [UserProfile, boolean] = await Promise.all([profilePromise, mfaPromise]) + const [profileResult, diceEnabled]: [UserProfile, boolean] = await Promise.all([profilePromise, dicePromise]) // make the changes we need based on the token - const output: UserProfile = profileFactoryCreate(profileResult, token, mfaEnabled) + const output: UserProfile = profileFactoryCreate(profileResult, token, diceEnabled) return output } From fe092c2c0247996c3647bdc5e218d0c2d185517d Mon Sep 17 00:00:00 2001 From: Brooke Date: Fri, 23 Dec 2022 11:41:01 -0800 Subject: [PATCH 5/6] TCA-852 remove console log --- .../course-details/course-curriculum/CourseCurriculum.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx b/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx index 51637a968..0dc1abd4e 100644 --- a/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx +++ b/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx @@ -77,7 +77,7 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp * Handle user click on start course/resume/login button */ const handleStartCourseClick: () => void = useCallback(() => { - console.debug(props.profile) + // if user is not logged in, redirect to login page if (!isLoggedIn) { // add a flag to the return url to show the academic policy modal @@ -90,7 +90,7 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp // let them know if (props.profile?.isWipro && !props.profile.diceEnabled) { // TODO - console.debug('user needs dice') + console.debug('TODO: user needs dice') return } From cbcfac493f74691bfbabe0bd64694fbca03eaea1 Mon Sep 17 00:00:00 2001 From: Brooke Date: Fri, 23 Dec 2022 11:41:45 -0800 Subject: [PATCH 6/6] TCA-852 fix grammar --- .../learn/course-details/course-curriculum/CourseCurriculum.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx b/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx index 0dc1abd4e..ab1d97727 100644 --- a/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx +++ b/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx @@ -87,7 +87,7 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp } // if the user is wipro and s/he hasn't set up DICE, - // let them know + // let the user know if (props.profile?.isWipro && !props.profile.diceEnabled) { // TODO console.debug('TODO: user needs dice')