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');
+ });
});