diff --git a/CHANGELOG b/CHANGELOG index 174a0978c..13f87e62d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,21 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +26.10.1 (2026-05-15) +==================== + +* Hotfix to take users to the links they clicked instead of the dashboard + +26.10.0 (2026-05-07) +==================== + +* OSF4I In-progress SSO Project - FE Piece + +26.9.2 (2026-05-06) +=================== + +* Hotfix to avoid errors on creating preprint versions + 26.9.1 (2026-04-28) =================== diff --git a/package-lock.json b/package-lock.json index ac558ab70..343846a54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "osf", - "version": "26.9.1", + "version": "26.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "osf", - "version": "26.9.1", + "version": "26.10.1", "dependencies": { "@angular/animations": "^21.2.7", "@angular/cdk": "^21.2.6", @@ -840,9 +840,9 @@ } }, "node_modules/@angular/ssr": { - "version": "21.2.7", - "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-21.2.7.tgz", - "integrity": "sha512-NhrkeD32s3H/jU9yJLqDy2JBNNatFyzqNkwieJw0waEvBRNbxXlcg5+g6rilcg2nHlH5hyzMQUzs7ZwZH9wCqg==", + "version": "21.2.12", + "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-21.2.12.tgz", + "integrity": "sha512-g3GZXWlS73TX87awmFUBuviWALida9t5g0iWZ2KZF+e3oaDFOp4JeA5eOsrHlNfd6CBIJItRTiy5SRXUuV1fjA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -6238,9 +6238,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -7726,13 +7726,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -7794,9 +7794,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -8227,9 +8227,9 @@ } }, "node_modules/hono": { - "version": "4.12.14", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", - "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "dev": true, "license": "MIT", "engines": { @@ -8526,9 +8526,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "engines": { @@ -10878,9 +10878,9 @@ } }, "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/package.json b/package.json index 3409621d3..9d623a9b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "osf", - "version": "26.9.1", + "version": "26.10.1", "scripts": { "ng": "ng", "analyze-bundle": "ng build --configuration=analyze-bundle && source-map-explorer dist/**/*.js --no-border-checks", diff --git a/src/app/core/interceptors/error.interceptor.spec.ts b/src/app/core/interceptors/error.interceptor.spec.ts index d8209e666..a082c289d 100644 --- a/src/app/core/interceptors/error.interceptor.spec.ts +++ b/src/app/core/interceptors/error.interceptor.spec.ts @@ -10,6 +10,7 @@ import { Router } from '@angular/router'; import { SENTRY_TOKEN } from '@core/provider/sentry.provider'; import { AuthService } from '@core/services/auth.service'; import { MaintenanceModeService } from '@core/services/maintenance-mode.service'; +import { UserSelectors } from '@core/store/user'; import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; @@ -22,6 +23,7 @@ import { } from '@testing/providers/maintenance-mode.service.mock'; import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; import { SentryMock, SentryMockType } from '@testing/providers/sentry-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; import { ViewOnlyLinkHelperMock, ViewOnlyLinkHelperMockType } from '@testing/providers/view-only-link-helper.mock'; @@ -37,7 +39,12 @@ describe('errorInterceptor', () => { let viewOnlyHelperMock: ViewOnlyLinkHelperMockType; let sentryMock: SentryMockType; - function setup(platformId: 'browser' | 'server' = 'browser', viewOnly = false, routerUrl = '/dashboard') { + function setup( + platformId: 'browser' | 'server' = 'browser', + viewOnly = false, + routerUrl = '/dashboard', + isAuthenticated = false + ) { router = RouterMockBuilder.create().withUrl(routerUrl).withNavigate(vi.fn().mockResolvedValue(true)).build(); toastServiceMock = ToastServiceMock.simple(); loaderServiceMock = new LoaderServiceMock(); @@ -50,6 +57,9 @@ describe('errorInterceptor', () => { providers: [ provideOSFCore(), provideLoaderServiceMock(loaderServiceMock), + provideMockStore({ + selectors: [{ selector: UserSelectors.isAuthenticated, value: isAuthenticated }], + }), MockProvider(Router, router), MockProvider(ToastService, toastServiceMock), MockProvider(AuthService, authServiceMock), @@ -115,15 +125,30 @@ describe('errorInterceptor', () => { expect(toastServiceMock.showError).not.toHaveBeenCalled(); }); - it('should logout on 401 in browser when not view-only', async () => { - setup('browser', false); + it('should navigate to sign in on 401 in browser when anonymous and not view-only', async () => { + setup('browser', false, '/dashboard', false); + const request = createRequest('/api/v2/nodes/abc'); + const error = new HttpErrorResponse({ status: 401, error: {}, url: request.url }); + + const caught = await runInterceptor(request, error); + + expect(caught?.status).toBe(401); + expect(authServiceMock.navigateToSignIn).toHaveBeenCalled(); + expect(authServiceMock.logout).not.toHaveBeenCalled(); + expect(loaderServiceMock.hide).not.toHaveBeenCalled(); + expect(toastServiceMock.showError).not.toHaveBeenCalled(); + }); + + it('should logout on 401 in browser when authenticated and not view-only', async () => { + setup('browser', false, '/dashboard', true); const request = createRequest('/api/v2/nodes/abc'); const error = new HttpErrorResponse({ status: 401, error: {}, url: request.url }); const caught = await runInterceptor(request, error); expect(caught?.status).toBe(401); - expect(authServiceMock.logout).toHaveBeenCalled(); + expect(authServiceMock.logout).toHaveBeenCalledWith(window.location.href); + expect(authServiceMock.navigateToSignIn).not.toHaveBeenCalled(); expect(loaderServiceMock.hide).not.toHaveBeenCalled(); expect(toastServiceMock.showError).not.toHaveBeenCalled(); }); diff --git a/src/app/core/interceptors/error.interceptor.ts b/src/app/core/interceptors/error.interceptor.ts index 7adb6a7c2..4a620df46 100644 --- a/src/app/core/interceptors/error.interceptor.ts +++ b/src/app/core/interceptors/error.interceptor.ts @@ -1,3 +1,5 @@ +import { Store } from '@ngxs/store'; + import { throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -11,6 +13,7 @@ import { MaintenanceResponse } from '@core/models/maintenance-response.model'; import { SENTRY_TOKEN } from '@core/provider/sentry.provider'; import { AuthService } from '@core/services/auth.service'; import { MaintenanceModeService } from '@core/services/maintenance-mode.service'; +import { UserSelectors } from '@core/store/user'; import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; @@ -26,6 +29,7 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { const sentry = inject(SENTRY_TOKEN); const platformId = inject(PLATFORM_ID); const viewOnlyHelper = inject(ViewOnlyLinkHelperService); + const store = inject(Store); return next(req).pipe( catchError((error: HttpErrorResponse) => { @@ -69,7 +73,11 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { if (error.status === 401) { if (!viewOnlyHelper.hasViewOnlyParam(router)) { if (isPlatformBrowser(platformId)) { - authService.logout(); + if (store.selectSnapshot(UserSelectors.isAuthenticated)) { + authService.logout(window.location.href); + } else { + authService.navigateToSignIn(); + } } } return throwError(() => error); diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index 074e4d732..09f73bbb8 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -41,8 +41,14 @@ export class AuthService { } this.loaderService.show(); - const loginUrl = `${this.casUrl}/login?${urlParam({ service: `${this.webUrl}/login`, next: window.location.href })}`; - window.location.href = loginUrl; + + const serviceUrl = new URL(`${this.webUrl}/login`); + serviceUrl.searchParams.set('next', window.location.href); + + const loginUrl = new URL(`${this.casUrl}/login`); + loginUrl.searchParams.set('service', serviceUrl.toString()); + + window.location.href = loginUrl.toString(); } navigateToOrcidSignIn(): void { @@ -79,7 +85,7 @@ export class AuthService { if (isPlatformBrowser(this.platformId)) { this.cookieService.deleteAll(); - window.location.href = `${this.webUrl}/logout/?next=${encodeURIComponent(nextUrl || '/')}`; + window.location.href = `${this.webUrl}/logout/?next=${encodeURIComponent(nextUrl || `${window.location.origin}/`)}`; } } diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts index ae0b35450..760b76ce1 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts @@ -186,7 +186,9 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { const preprint = this.preprint(); if (!preprint) return false; - return this.hasAdminAccess() && preprint.datePublished && preprint.isLatestVersion; + const preprintIsRejected = preprint.reviewsState === ReviewsState.Rejected; + + return this.hasAdminAccess() && (preprint.datePublished || preprintIsRejected) && preprint.isLatestVersion; }); editButtonVisible = computed(() => { diff --git a/src/app/features/profile/components/profile-information/profile-information.component.spec.ts b/src/app/features/profile/components/profile-information/profile-information.component.spec.ts index 947edf462..f8ec5857e 100644 --- a/src/app/features/profile/components/profile-information/profile-information.component.spec.ts +++ b/src/app/features/profile/components/profile-information/profile-information.component.spec.ts @@ -37,6 +37,7 @@ describe('ProfileInformationComponent', () => { }, institutionalRequestAccessEnabled: true, logoPath: 'logo.png', + sso_availability: 'Public', }, ]; diff --git a/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts b/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts index 2bf6f7d49..5b037e8f7 100644 --- a/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts @@ -64,6 +64,7 @@ describe('AddComponentDialogComponent', () => { assets: { logo: '', logo_rounded: '', banner: '' }, institutionalRequestAccessEnabled: false, logoPath: '', + sso_availability: 'Public', }, { id: 'inst-2', @@ -76,6 +77,7 @@ describe('AddComponentDialogComponent', () => { assets: { logo: '', logo_rounded: '', banner: '' }, institutionalRequestAccessEnabled: false, logoPath: '', + sso_availability: 'Public', }, ]; @@ -91,6 +93,7 @@ describe('AddComponentDialogComponent', () => { assets: { logo: '', logo_rounded: '', banner: '' }, institutionalRequestAccessEnabled: false, logoPath: '', + sso_availability: 'Public', }, ]; diff --git a/src/app/shared/components/affiliated-institution-select/affiliated-institution-select.component.spec.ts b/src/app/shared/components/affiliated-institution-select/affiliated-institution-select.component.spec.ts index 836088967..1897a7660 100644 --- a/src/app/shared/components/affiliated-institution-select/affiliated-institution-select.component.spec.ts +++ b/src/app/shared/components/affiliated-institution-select/affiliated-institution-select.component.spec.ts @@ -68,6 +68,7 @@ describe('AffiliatedInstitutionSelectComponent', () => { }, institutionalRequestAccessEnabled: false, logoPath: '/logos/unavailable.png', + sso_availability: 'Unavailable', }; fixture.componentRef.setInput('institutions', mockInstitutions); @@ -134,6 +135,7 @@ describe('AffiliatedInstitutionSelectComponent', () => { }, institutionalRequestAccessEnabled: false, logoPath: '/logos/unavailable.png', + sso_availability: 'Unavailable', }; fixture.componentRef.setInput('institutions', mockInstitutions); @@ -181,6 +183,7 @@ describe('AffiliatedInstitutionSelectComponent', () => { }, institutionalRequestAccessEnabled: false, logoPath: '/logos/unavailable.png', + sso_availability: 'Unavailable', }; fixture.componentRef.setInput('institutions', mockInstitutions); diff --git a/src/app/shared/mappers/institutions/institutions.mapper.ts b/src/app/shared/mappers/institutions/institutions.mapper.ts index f3552702a..172faacf8 100644 --- a/src/app/shared/mappers/institutions/institutions.mapper.ts +++ b/src/app/shared/mappers/institutions/institutions.mapper.ts @@ -25,6 +25,7 @@ export class InstitutionsMapper { logoPath: data.attributes.logo_path, userMetricsUrl: data.relationships?.user_metrics?.links?.related?.href, linkToExternalReportsArchive: data.attributes.link_to_external_reports_archive, + sso_availability: data.attributes.sso_availability, }; } diff --git a/src/app/shared/models/institutions/institution-json-api.model.ts b/src/app/shared/models/institutions/institution-json-api.model.ts index cedc24e36..63a7f7a3c 100644 --- a/src/app/shared/models/institutions/institution-json-api.model.ts +++ b/src/app/shared/models/institutions/institution-json-api.model.ts @@ -23,6 +23,7 @@ interface InstitutionAttributesJsonApi { institutional_request_access_enabled: boolean; logo_path: string; link_to_external_reports_archive: string; + sso_availability: string; } interface InstitutionLinksJsonApi { diff --git a/src/app/shared/models/institutions/institutions.model.ts b/src/app/shared/models/institutions/institutions.model.ts index f00b4a278..aaee35b7d 100644 --- a/src/app/shared/models/institutions/institutions.model.ts +++ b/src/app/shared/models/institutions/institutions.model.ts @@ -16,6 +16,7 @@ export interface Institution { logoPath: string; userMetricsUrl?: string; linkToExternalReportsArchive?: string; + sso_availability: string; } export interface InstitutionAssets { diff --git a/src/testing/mocks/institution.mock.ts b/src/testing/mocks/institution.mock.ts index fa918fabd..390852767 100644 --- a/src/testing/mocks/institution.mock.ts +++ b/src/testing/mocks/institution.mock.ts @@ -13,4 +13,5 @@ export const MOCK_INSTITUTION = { }, institutionalRequestAccessEnabled: true, logoPath: 'https://mockinstitution.org/logo.png', + sso_availability: 'Public', };