Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
614bf23
fix(nav-menu): fixed translations for nav menu items
nsemets Feb 2, 2026
4b579d3
fix(resource-card): fixed contributors display for search
nsemets Feb 2, 2026
95f82db
Merge pull request #869 from nsemets/hotfix/nav-menu-translations
adlius Feb 3, 2026
1831419
Merge pull request #871 from nsemets/hotfix/search-contributors-fix
adlius Feb 3, 2026
a938fe4
feat(release): Bump version no. Add CHANGELOG
adlius Feb 3, 2026
d92ff04
Merge branch 'hotfix/26.2.1'
adlius Feb 3, 2026
e7cb9a0
Merge tag '26.2.1' into develop
adlius Feb 3, 2026
01f5a10
feat(signposting): add signposting service (#873)
futa-ikeda Feb 5, 2026
6e4f41b
fix(files): fixed move file to parent
nsemets Feb 6, 2026
2eadda3
feat(signposting): add signposting links to content landing pages (#875)
futa-ikeda Feb 6, 2026
b93fe19
Merge pull request #876 from nsemets/fix/ENG-10217
adlius Feb 10, 2026
64457ef
Merge branch 'hotfix/26.2.2'
adlius Feb 11, 2026
ebe88f6
Merge tag '26.2.2' into develop
adlius Feb 11, 2026
ce0774f
feat(signposting): add signposting to project/registration metadata p…
futa-ikeda Feb 12, 2026
b86b954
fix(signposting): fix linkset-json format (#882)
futa-ikeda Feb 13, 2026
bd98668
[ENG-10316] SSR config updates (#889)
nsemets Feb 19, 2026
79c4ef6
fix(ssr): added throttle token and updated config load
nsemets Feb 23, 2026
30fc432
Merge pull request #891 from CenterForOpenScience/feature/fair-signpo…
adlius Feb 24, 2026
77d7a5f
feat(release): Bump version no. Add CHANGELOG
adlius Feb 24, 2026
af20cc7
Merge branch 'release/26.3.0'
adlius Feb 24, 2026
f4685f6
Merge tag '26.3.0' into develop
adlius Feb 24, 2026
8bc53fa
fix(interceptor): Add custom request header to prevent 403 redirects
futa-ikeda Feb 25, 2026
2ea743a
Merge pull request #896 from futa-ikeda/hotfix/vol-redirect
adlius Feb 25, 2026
a865d78
feat(release): Bump version no. Add CHANGELOG
adlius Feb 25, 2026
cc84360
Merge branch 'hotfix/26.3.1'
adlius Feb 25, 2026
db69159
Merge tag '26.3.1' into develop
adlius Feb 25, 2026
fbd9958
[ENG-10054] feature/ror-migration (#897)
felliott Feb 26, 2026
0f3648b
feat(release): bump version and update changelog
felliott Feb 26, 2026
aaeb9cb
Merge branch 'main' into develop
felliott Feb 26, 2026
74ac9d2
fix(docker): updated docker file
nsemets Feb 27, 2026
cdeeae4
fix(i18n): added logic for ssr
nsemets Mar 5, 2026
71f3a65
fix(commands): updated commands for angular serve
nsemets Mar 5, 2026
4820291
fix(meetings): fixed issue with duplicated data
nsemets Mar 5, 2026
83b7340
Merge remote-tracking branch 'upstream/feature/pbs-26-2' into pbs-26-2
nsemets Mar 5, 2026
3b0e785
fix(token): t
nsemets Mar 5, 2026
6bae6d2
fix(commands): updated commands for csr and ssr
nsemets Mar 5, 2026
752f4fa
Merge remote-tracking branch 'origin/develop' into fix/ssr-throttle-t…
nsemets Mar 10, 2026
e8b9929
Merge remote-tracking branch 'upstream/feature/pbs-26-2' into pbs-26-2
nsemets Mar 10, 2026
43ffb69
fix(search-filters): fixed duplication of filters
nsemets Mar 11, 2026
494fc22
fix(wiki): added wiki to ssr
nsemets Mar 12, 2026
e7ac654
Merge pull request #893 from nsemets/fix/ssr-throttle-token
adlius Mar 13, 2026
e35f2aa
feat(release): Bump version no. Add CHANGELOG
adlius Mar 13, 2026
71b576c
Merge branch 'release/26.5.0'
adlius Mar 13, 2026
c7f35df
Merge tag '26.5.0' into develop
adlius Mar 13, 2026
dcfe028
Merge remote-tracking branch 'upstream/feature/pbs-26-2' into pbs-26-2
nsemets Mar 13, 2026
b37bb83
Merge remote-tracking branch 'upstream/develop' into fix/develop-merge
nsemets Mar 16, 2026
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
25 changes: 25 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.

26.5.0 (2026-02-26)
===================

* Config updates to enable Angular SSR

26.4.0 (2026-02-26)
===================

* Search and fetch funder ids from ROR instead of CrossRef

26.3.1 (2026-02-25)
===================

* Hotfix to prevent 403 error when fetching metadata from causing a redirect

26.3.0 (2026-02-24)
===================

* FAIR Signposting

26.2.1 (2026-02-03)
===================

* Hotfix for navigation translations and contributor search

26.2.0 (2026-01-29)
===================

Expand Down
60 changes: 19 additions & 41 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,59 +1,37 @@
# Build
FROM node:22-alpine AS build

# Dependencies stage
FROM node:22-alpine AS deps
WORKDIR /app

COPY package*.json ./
RUN npm install
RUN npm ci --no-audit --no-fund

# Build stage (SSR build output)
FROM deps AS build
COPY . .
RUN NG_BUILD_OPTIMIZE_CHUNKS=1 npx ng build --configuration=ssr --verbose

RUN npm link @angular/cli
RUN NG_BUILD_OPTIMIZE_CHUNKS=1 ng build --verbose

# Dist
FROM node:22-alpine AS dist

WORKDIR /code

COPY --from=build /app/dist /code/dist

# SSR
FROM node:22-alpine AS ssr

# SSR runtime stage
FROM build AS ssr
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

RUN npm link @angular/cli
RUN NG_BUILD_OPTIMIZE_CHUNKS=1 ng build --configuration=ssr --verbose

RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund

RUN npm prune --omit=dev --no-audit --no-fund
EXPOSE 4000

ENV PORT=4000

CMD ["node", "dist/osf/server/server.mjs"]

# Dev - run only
FROM build AS dev
# Static dist artifact stage
FROM node:22-alpine AS dist
WORKDIR /code
COPY --from=build /app/dist /code/dist

# Dev server stage
FROM deps AS dev
COPY . .
EXPOSE 4200
CMD ["npx", "ng", "serve", "--host", "0.0.0.0"]

CMD ["ng", "serve"]

# Local Development - coding
# Local development stage
FROM node:22-alpine AS local-dev
WORKDIR /app

# Install deps in the image (kept in container)
COPY package*.json ./
# COPY package-lock.docker.json ./package-lock.json
RUN npm ci --no-audit --no-fund

# Expose Angular dev server
EXPOSE 4200
CMD ["npx", "ng", "serve", "--host", "0.0.0.0"]
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "osf",
"version": "26.2.0",
"version": "26.5.0",
"scripts": {
"ng": "ng",
"analyze-bundle": "ng build --configuration=analyze-bundle && source-map-explorer dist/**/*.js --no-border-checks",
Expand Down
1 change: 1 addition & 0 deletions src/@types/ace-builds.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'ace-builds/src-noconflict/ext-language_tools';
33 changes: 32 additions & 1 deletion src/app/app.config.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,42 @@ import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { provideServerRouting } from '@angular/ssr';

import { SSR_CONFIG } from '@core/constants/ssr-config.token';
import { ConfigModel } from '@core/models/config.model';

import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';

import { existsSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

function loadSsrConfig(): ConfigModel {
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const configPath = resolve(serverDistFolder, '../browser/assets/config/config.json');

let config = {} as ConfigModel;

if (existsSync(configPath)) {
try {
config = JSON.parse(readFileSync(configPath, 'utf-8'));
} catch {
config = {} as ConfigModel;
}
}

return {
...config,
throttleToken: process.env['THROTTLE_TOKEN'] || '',
} as ConfigModel;
}

const serverConfig: ApplicationConfig = {
providers: [provideServerRendering(), provideServerRouting(serverRoutes)],
providers: [
provideServerRendering(),
provideServerRouting(serverRoutes),
{ provide: SSR_CONFIG, useFactory: loadSsrConfig },
],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);
8 changes: 8 additions & 0 deletions src/app/app.routes.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ export const serverRoutes: ServerRoute[] = [
path: ':id/overview',
renderMode: RenderMode.Server,
},
{
path: ':id/metadata/:recordId',
renderMode: RenderMode.Server,
},
{
path: ':id/wiki',
renderMode: RenderMode.Server,
},
{
path: ':id/files/**',
renderMode: RenderMode.Server,
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/components/nav-menu/nav-menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ export class NavMenuComponent {
private readonly isAuthenticated = select(UserSelectors.isAuthenticated);
private readonly currentResource = select(CurrentResourceSelectors.getCurrentResource);
private readonly provider = select(ProviderSelectors.getCurrentProvider);
private readonly translationsReady = toSignal(this.translateService.stream('navigation.overview'));

readonly actions = createDispatchMap({ getResourceDetails: GetResourceDetails });

readonly mainMenuItems = computed(() => {
this.translationsReady();
const isAuthenticated = this.isAuthenticated();
const filtered = filterMenuItems(MENU_ITEMS, isAuthenticated);

Expand Down
5 changes: 5 additions & 0 deletions src/app/core/constants/ssr-config.token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { InjectionToken } from '@angular/core';

import { ConfigModel } from '@core/models/config.model';

export const SSR_CONFIG = new InjectionToken<ConfigModel>('SSR_CONFIG');
12 changes: 11 additions & 1 deletion src/app/core/helpers/i18n.helper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { TranslateLoader, TranslateModuleConfig } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';

import { isPlatformServer } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { inject, PLATFORM_ID } from '@angular/core';

import { ENVIRONMENT } from '@core/provider/environment.provider';

function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
const platformId = inject(PLATFORM_ID);
const environment = inject(ENVIRONMENT);
const basePrefix = '/assets/i18n/';
const webUrl = environment.webUrl?.replace(/\/+$/, '') ?? '';
const prefix = isPlatformServer(platformId) && webUrl ? `${webUrl}${basePrefix}` : basePrefix;

return new TranslateHttpLoader(http, prefix, '.json');
}

export const provideTranslation = (): TranslateModuleConfig => ({
Expand Down
78 changes: 58 additions & 20 deletions src/app/core/interceptors/auth.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,33 @@ import { MockProvider } from 'ng-mocks';
import { of } from 'rxjs';

import { HttpRequest } from '@angular/common/http';
import { runInInjectionContext } from '@angular/core';
import { PLATFORM_ID, runInInjectionContext } from '@angular/core';
import { TestBed } from '@angular/core/testing';

import { ENVIRONMENT } from '@core/provider/environment.provider';
import { EnvironmentModel } from '@osf/shared/models/environment.model';

import { authInterceptor } from './auth.interceptor';

describe('authInterceptor', () => {
let cookieService: CookieService;
let mockHandler: jest.Mock;
let cookieServiceMock: { get: jest.Mock };

beforeEach(() => {
mockHandler = jest.fn();
const setup = (platformId = 'browser', environmentOverrides: Partial<EnvironmentModel> = {}) => {
cookieServiceMock = { get: jest.fn() };

TestBed.configureTestingModule({
providers: [
MockProvider(CookieService, {
get: jest.fn(),
}),
{
provide: 'PLATFORM_ID',
useValue: 'browser',
},
{
provide: 'REQUEST',
useValue: null,
},
MockProvider(CookieService, cookieServiceMock),
MockProvider(PLATFORM_ID, platformId),
MockProvider(ENVIRONMENT, { throttleToken: '', ...environmentOverrides } as EnvironmentModel),
],
});

cookieService = TestBed.inject(CookieService);
};

beforeEach(() => {
jest.clearAllMocks();
});

Expand All @@ -44,12 +42,13 @@ describe('authInterceptor', () => {
};

const createHandler = () => {
const handler = mockHandler.mockReturnValue(of({}));
const handler = jest.fn().mockReturnValue(of({}));
return handler;
};

it('should skip CrossRef funders API requests', () => {
const request = createRequest('/api.crossref.org/funders/10.13039/100000001');
it('should skip ROR funders API requests', () => {
setup();
const request = createRequest('https://api.ror.org/v2');
const handler = createHandler();

runInInjectionContext(TestBed, () => authInterceptor(request, handler));
Expand All @@ -60,6 +59,7 @@ describe('authInterceptor', () => {
});

it('should set Accept header to */* for text response type', () => {
setup();
const request = createRequest('/api/v2/projects/', { responseType: 'text' });
const handler = createHandler();

Expand All @@ -71,6 +71,7 @@ describe('authInterceptor', () => {
});

it('should set Accept header to API version for json response type', () => {
setup();
const request = createRequest('/api/v2/projects/', { responseType: 'json' });
const handler = createHandler();

Expand All @@ -82,6 +83,7 @@ describe('authInterceptor', () => {
});

it('should set Content-Type header when not present', () => {
setup();
const request = createRequest('/api/v2/projects/');
const handler = createHandler();

Expand All @@ -93,6 +95,7 @@ describe('authInterceptor', () => {
});

it('should not override existing Content-Type header', () => {
setup();
const request = createRequest('/api/v2/projects/');
const requestWithHeaders = request.clone({
setHeaders: { 'Content-Type': 'application/json' },
Expand All @@ -107,7 +110,8 @@ describe('authInterceptor', () => {
});

it('should add CSRF token and withCredentials in browser platform', () => {
jest.spyOn(cookieService, 'get').mockReturnValue('csrf-token-123');
setup();
cookieServiceMock.get.mockReturnValue('csrf-token-123');

const request = createRequest('/api/v2/projects/');
const handler = createHandler();
Expand All @@ -122,7 +126,8 @@ describe('authInterceptor', () => {
});

it('should not add CSRF token when not available in browser platform', () => {
jest.spyOn(cookieService, 'get').mockReturnValue('');
setup();
cookieServiceMock.get.mockReturnValue('');

const request = createRequest('/api/v2/projects/');
const handler = createHandler();
Expand All @@ -135,4 +140,37 @@ describe('authInterceptor', () => {
expect(modifiedRequest.headers.has('X-CSRFToken')).toBe(false);
expect(modifiedRequest.withCredentials).toBe(true);
});

it('should not add X-Throttle-Token on browser platform', () => {
setup('browser', { throttleToken: 'test-token' });
const request = createRequest('/api/v2/projects/');
const handler = createHandler();

runInInjectionContext(TestBed, () => authInterceptor(request, handler));

const modifiedRequest = handler.mock.calls[0][0];
expect(modifiedRequest.headers.has('X-Throttle-Token')).toBe(false);
});

it('should add X-Throttle-Token on server platform when token is present', () => {
setup('server', { throttleToken: 'test-token' });
const request = createRequest('/api/v2/projects/');
const handler = createHandler();

runInInjectionContext(TestBed, () => authInterceptor(request, handler));

const modifiedRequest = handler.mock.calls[0][0];
expect(modifiedRequest.headers.get('X-Throttle-Token')).toBe('test-token');
});

it('should not add X-Throttle-Token on server platform when token is empty', () => {
setup('server', { throttleToken: '' });
const request = createRequest('/api/v2/projects/');
const handler = createHandler();

runInInjectionContext(TestBed, () => authInterceptor(request, handler));

const modifiedRequest = handler.mock.calls[0][0];
expect(modifiedRequest.headers.has('X-Throttle-Token')).toBe(false);
});
});
Loading
Loading