diff --git a/packages/frontend/app/controllers/application.js b/packages/frontend/app/controllers/application.js index 043760c52f..111f1c362f 100644 --- a/packages/frontend/app/controllers/application.js +++ b/packages/frontend/app/controllers/application.js @@ -44,6 +44,15 @@ export default class ApplicationController extends Controller { return ''; } + get hasLtiApplicationScope() { + const applicationScope = this.currentUser.applicationScope ?? ''; + return applicationScope.startsWith('lti-'); + } + + get hasNavigation() { + return this.currentUser.performsNonLearnerFunction && !this.hasLtiApplicationScope; + } + @action clearErrors() { this.errors = []; diff --git a/packages/frontend/app/templates/application.gjs b/packages/frontend/app/templates/application.gjs index 251597daad..43c495c453 100644 --- a/packages/frontend/app/templates/application.gjs +++ b/packages/frontend/app/templates/application.gjs @@ -14,29 +14,26 @@ import FlashMessages from 'frontend/components/flash-messages'; -
- -
diff --git a/packages/frontend/tests/acceptance/application-layout-test.js b/packages/frontend/tests/acceptance/application-layout-test.js new file mode 100644 index 0000000000..c84baafa8a --- /dev/null +++ b/packages/frontend/tests/acceptance/application-layout-test.js @@ -0,0 +1,32 @@ +import { module, test } from 'qunit'; +import { visit } from '@ember/test-helpers'; +import { setupAuthentication } from 'ilios-common'; +import { setupApplicationTest, takeScreenshot } from 'frontend/tests/helpers'; + +module('Acceptance | Application layout', function (hooks) { + setupApplicationTest(hooks); + + test('layout with LTI-scoped authentication token', async function (assert) { + await setupAuthentication({}, true, { application_scope: 'lti-dashboard' }); + await visit('/'); + assert.dom('.application-wrapper').doesNotHaveClass('show-navigation'); + assert.dom('header.ilios-header').doesNotExist(); + assert.dom('.ilios-logo').doesNotExist(); + assert.dom('[data-test-ilios-navigation]').doesNotExist(); + assert.dom('#main').exists(); + assert.dom('footer.ilios-footer').doesNotExist(); + await takeScreenshot(assert); + }); + + test('layout with non LTI-scoped authentication token', async function (assert) { + await setupAuthentication({}, true); + await visit('/'); + assert.dom('.application-wrapper').hasClass('show-navigation'); + assert.dom('header.ilios-header').exists(); + assert.dom('.ilios-logo').exists(); + assert.dom('[data-test-ilios-navigation]').exists(); + assert.dom('#main').exists(); + assert.dom('footer.ilios-footer').exists(); + await takeScreenshot(assert); + }); +}); diff --git a/packages/ilios-common/addon-test-support/ilios-common/helpers/setup-authentication.js b/packages/ilios-common/addon-test-support/ilios-common/helpers/setup-authentication.js index 4a0e6ed49a..f1be617d1e 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/helpers/setup-authentication.js +++ b/packages/ilios-common/addon-test-support/ilios-common/helpers/setup-authentication.js @@ -6,9 +6,10 @@ const defaultUserId = 100; export default async function ( userObject = { id: defaultUserId }, performsNonLearnerFunction = false, + jwtOptions = {}, ) { const userId = userObject && 'id' in userObject ? userObject.id : defaultUserId; - const jwtObject = { + let jwtObject = { user_id: userId, }; const nonLearnerFunctions = [ @@ -31,6 +32,13 @@ export default async function ( if (performsNonLearnerFunction || hasNonLearnerFunctionInPassedData) { jwtObject['performs_non_learner_function'] = true; } + // KLUDGE! + // merge options into JWT. + // this is quite bad, since this has the potential to clobber previously set values. + // alas, rewriting the whole thing is not in scope, so let's cheese it like this for now. + // TODO: clean this up [ST 2026/06/03] + jwtObject = { ...jwtObject, ...jwtOptions }; + const encodedData = window.btoa('') + '.' + window.btoa(JSON.stringify(jwtObject)) + '.'; const token = { jwt: encodedData, diff --git a/packages/ilios-common/addon/services/current-user.js b/packages/ilios-common/addon/services/current-user.js index 935d859a91..af7e72fcb7 100644 --- a/packages/ilios-common/addon/services/current-user.js +++ b/packages/ilios-common/addon/services/current-user.js @@ -85,21 +85,40 @@ export default class CurrentUserService extends Service { }); } - getBooleanAttributeFromToken(attribute) { + /** + * Returns the decoded JWT from the current user session. + * @returns {object|null} + */ + get decodedJwt() { const session = this.session; if (isEmpty(session)) { - return false; + return null; } const jwt = session.get('data.authenticated.jwt'); if (isEmpty(jwt)) { - return false; + return null; } - const obj = jwtDecode(jwt); + return jwtDecode(jwt); + } - return !!get(obj, attribute); + getBooleanAttributeFromToken(attribute) { + if (this.decodedJwt) { + return !!get(this.decodedJwt, attribute); + } + return false; } + + get applicationScope() { + if (!this.decodedJwt) { + return null; + } + return Object.hasOwn(this.decodedJwt, 'application_scope') + ? this.decodedJwt['application_scope'] + : null; + } + get isRoot() { return this.getBooleanAttributeFromToken('is_root'); } diff --git a/packages/test-app/tests/integration/services/current-user-test.js b/packages/test-app/tests/integration/services/current-user-test.js index bb89606409..3bdda9547f 100644 --- a/packages/test-app/tests/integration/services/current-user-test.js +++ b/packages/test-app/tests/integration/services/current-user-test.js @@ -297,4 +297,17 @@ module('Integration | Service | Current User', function (hooks) { skip('requireNonLearner', function (/* assert */) { // TODO: implement. }); + + test('application scope', async function (assert) { + // check the current session user's JWT for an application scope value. it doesn't have one. + const subject = this.owner.lookup('service:current-user'); + assert.strictEqual(subject.applicationScope, null); + + // now re-authenticate with a token that carries an "application_scope" attribute. + await invalidateSession(); + await authenticateSession({ + jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwidXNlcl9pZCI6MTAwLCJhcHBsaWNhdGlvbl9zY29wZSI6Imx0aS1kYXNoY2FtIn0.hM_QXikx6hAt-dhorqDp2QKFAHMIYUGkS74Guug55lE', + }); + assert.strictEqual(subject.applicationScope, 'lti-dashcam'); + }); });