Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7016c3f
show an OIDC profile widget on user preferences page based on config
marius-mather Nov 13, 2025
5d7b386
update logic in user preferences model - shouldn't allow editing emai…
marius-mather Nov 13, 2025
8155fad
add UserOidcProfile component for displaying profile info, with link …
marius-mather Nov 14, 2025
4e03dfa
add oidc_profile_url to config schema
marius-mather Nov 14, 2025
d86d3a4
expose OIDC profile URL in config API
marius-mather Nov 14, 2025
02c288c
update logic for showing email/username fields based on whether local…
marius-mather Nov 14, 2025
019c9e3
Merge remote-tracking branch 'upstream/dev' into oidc-external-profile
marius-mather Nov 14, 2025
07a63c0
run client formatter
marius-mather Nov 14, 2025
201b5fe
run make format
marius-mather Nov 14, 2025
56b2cb7
add profile url and check to ExternalIDHelper file
marius-mather Nov 24, 2025
80d2d51
show provider name on button where possible
marius-mather Nov 24, 2025
7e3e76c
update logic for showing OIDC user profile
marius-mather Nov 24, 2025
dc9bc7b
move profile_url config to OIDC backend
marius-mather Nov 24, 2025
a704491
Merge remote-tracking branch 'upstream/dev' into oidc-external-profile
marius-mather Nov 24, 2025
b2f2db3
Merge remote-tracking branch 'upstream/dev' into oidc-external-profile
marius-mather Nov 24, 2025
017d4b6
lint fixes
marius-mather Nov 24, 2025
0e69712
type-checking fixes
marius-mather Nov 24, 2025
33c2f6a
unit tests for UserOidcProfile
marius-mather Nov 24, 2025
68f4b37
prettier fixes
marius-mather Nov 24, 2025
d52f0a9
rework component test
marius-mather Nov 24, 2025
339db8b
fix logic for showing OIDC profile widget
marius-mather Nov 24, 2025
6db289c
fix prop order
marius-mather Nov 24, 2025
e0783da
add tests of UserPreferences display logic
marius-mather Nov 24, 2025
21a741a
test disabling password widget
marius-mather Nov 25, 2025
f8af6de
rename variable to make clear we're disabling profile editing
marius-mather Nov 27, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type OIDCConfig = Record<
label?: string;
custom_button_text?: string;
end_user_registration_endpoint?: string;
profile_url?: string;
}
>;

Expand All @@ -21,6 +22,7 @@ export type OIDCConfigWithRegistration = Record<
label?: string;
custom_button_text?: string;
end_user_registration_endpoint: string;
profile_url?: string;
}
>;

Expand Down Expand Up @@ -96,6 +98,10 @@ export function isOnlyOneOIDCProviderConfigured(config: OIDCConfig): boolean {
return Object.keys(config).length === 1;
}

export function hasSingleOidcProfile(config: OIDCConfig): boolean {
return isOnlyOneOIDCProviderConfigured(config) && !!config.profile_url;
}

export async function redirectToSingleProvider(config: OIDCConfig): Promise<string | null> {
const providers = Object.keys(config);

Expand Down
70 changes: 70 additions & 0 deletions client/src/components/User/UserOidcProfile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createTestingPinia } from "@pinia/testing";
import { getFakeRegisteredUser } from "@tests/test-data";
import { getLocalVue } from "@tests/vitest/helpers";
import { mount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
import { ref } from "vue";
import VueRouter from "vue-router";

import { useUserStore } from "@/stores/userStore";

import UserOidcProfile from "./UserOidcProfile.vue";
import GButton from "@/components/BaseComponents/GButton.vue";

const PROFILE_URL = "https://profile.example.com";
const MOCK_CONFIG = {
oidc_profile_url: PROFILE_URL,
oidc: {
provider: {
label: "Example Provider",
},
},
};

vi.mock("@/composables/config", () => ({
useConfig: vi.fn(() => ({
config: ref(MOCK_CONFIG),
isConfigLoaded: ref(true),
})),
}));

const localVue = getLocalVue();
localVue.use(VueRouter);

function mountProfile(userOverrides = {}) {
const pinia = createTestingPinia({ createSpy: vi.fn });
const userStore = useUserStore(pinia);
userStore.currentUser = getFakeRegisteredUser(userOverrides);
const router = new VueRouter();

return mount(UserOidcProfile, {
localVue,
pinia,
router,
stubs: {
FontAwesomeIcon: true,
},
});
}

describe("UserOidcProfile", () => {
it("shows the profile link from config", async () => {
const wrapper = mountProfile();
await wrapper.vm.$nextTick();

const profileButton = wrapper.findComponent(GButton);
expect(profileButton.exists()).toBe(true);
expect(profileButton.props("href")).toBe(PROFILE_URL);
});

it("displays username and email from the user store", async () => {
const username = "oidc_user";
const email = "oidc_user@example.com";
const wrapper = mountProfile({ username, email });
await wrapper.vm.$nextTick();

const details = wrapper.findAll("dd");
expect(details.at(0)!.text()).toBe(email);
expect(details.at(1)!.text()).toBe(username);
});
});
106 changes: 106 additions & 0 deletions client/src/components/User/UserOidcProfile.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<script setup lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faUser } from "font-awesome-6";
import { storeToRefs } from "pinia";
import { computed } from "vue";

import { isRegisteredUser } from "@/api";
import { useConfig } from "@/composables/config";
import { useUserStore } from "@/stores/userStore";
import localize from "@/utils/localization";

import GButton from "@/components/BaseComponents/GButton.vue";
import BreadcrumbHeading from "@/components/Common/BreadcrumbHeading.vue";

const { config, isConfigLoaded } = useConfig(true);

const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);

const breadcrumbItems = computed(() => [{ title: "User Preferences", to: "/user" }, { title: "Manage Profile" }]);

const username = computed(() => {
if (isRegisteredUser(currentUser.value)) {
return currentUser.value.username;
}
return localize("Not available");
});
const email = computed(() => {
if (isRegisteredUser(currentUser.value)) {
return currentUser.value.email;
}
return localize("Not available");
});
const profileUrl = computed(() => {
if (!isConfigLoaded.value) {
// Need to return a value to ensure the GButton is
// rendered as a link
return "#";
}
return config.value.oidc_profile_url || "";
});
const profileButtonLabel = computed(() => {
const providers = Object.keys(config.value.oidc) || [];
if (providers.length === 0 || providers === null) {
return null;
}
const providerConfig = config.value.oidc[providers[0]!];
const providerLabel = providerConfig.custom_button_text || providerConfig.label;
if (providerLabel) {
return `Update profile details at ${providerLabel}`;
}
return "Update profile details";
});

const buttonDisabled = computed(() => !isConfigLoaded.value || !profileUrl.value);
</script>

<template>
<div>
<BreadcrumbHeading :items="breadcrumbItems" />
<div id="manage-profile-card" class="ui-portlet-section">
<div class="portlet-header">
<span class="portlet-title">
<FontAwesomeIcon :icon="faUser" fixed-width class="mr-1" />
<span class="portlet-title-text">{{ localize("Manage Profile") }}</span>
</span>
</div>
<div class="portlet-content">
<dl class="d-flex flex-column flex-gapy-1">
<div class="my-2">
<dt class="text-md-left">{{ localize("Email") }}</dt>
<dd>{{ email }}</dd>
</div>
<div class="my-2">
<dt class="text-md-left">{{ localize("Username") }}</dt>
<dd>{{ username }}</dd>
<span class="text-sm-left">
<em>
{{
localize(
"Your username is an identifier that will be used to generate addresses for information you share publicly.",
)
}}
</em>
</span>
</div>
<div class="my-2">
<dt>{{ localize("Password") }}</dt>
<dd>●●●●●●●●●●</dd>
</div>
</dl>

<GButton
color="blue"
size="medium"
class="mt-3"
:disabled="buttonDisabled"
:href="profileUrl"
target="_blank">
<span>{{ localize(profileButtonLabel) }}</span>
<span class="mr-1 fa fa-external-link-alt" />
</GButton>
</div>
</div>
</div>
</template>
Loading
Loading