Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,305 changes: 697 additions & 608 deletions package-lock.json

Large diffs are not rendered by default.

59 changes: 31 additions & 28 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,28 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^21.2.19",
"@angular/animations": "^21.2.25",
"@angular/cdk": "^21.2.14",
"@angular/common": "^21.2.19",
"@angular/compiler": "^21.2.19",
"@angular/core": "^21.2.19",
"@angular/forms": "^21.2.19",
"@angular/platform-browser": "^21.2.19",
"@angular/platform-browser-dynamic": "^21.2.19",
"@angular/platform-server": "^21.2.19",
"@angular/router": "^21.2.19",
"@angular/ssr": "^21.2.20",
"@angular/common": "^21.2.25",
"@angular/compiler": "^21.2.25",
"@angular/core": "^21.2.25",
"@angular/forms": "^21.2.25",
"@angular/platform-browser": "^21.2.25",
"@angular/platform-browser-dynamic": "^21.2.25",
"@angular/platform-server": "^21.2.25",
"@angular/router": "^21.2.25",
"@angular/ssr": "^21.2.24",
"@centerforopenscience/markdown-it-atrules": "^0.1.2",
"@citation-js/core": "^0.7.21",
"@citation-js/plugin-csl": "^0.7.21",
"@fortawesome/fontawesome-free": "^7.2.0",
"@fortawesome/fontawesome-free": "^7.3.1",
"@mdit/plugin-img-size": "0.23.1",
"@newrelic/browser-agent": "^1.310.1",
"@newrelic/browser-agent": "^1.323.0",
"@ngx-translate/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0",
"@ngxs/store": "^21.0.0",
"@primeuix/themes": "^2.0.3",
"@sentry/angular": "^10.42.0",
"@sentry/angular": "^10.75.3",
"@traptitech/markdown-it-katex": "^3.6.0",
"ace-builds": "1.43.6",
"angular-google-tag-manager": "^1.13.0",
Expand All @@ -70,42 +70,45 @@
"ngx-markdown-editor": "^5.3.4",
"primeflex": "^4.0.0",
"primeicons": "^7.0.0",
"primeng": "^21.1.3",
"primeng": "^21.1.10",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular-eslint/eslint-plugin": "^21.3.0",
"@angular-eslint/eslint-plugin-template": "^21.3.0",
"@angular-eslint/template-parser": "^21.3.0",
"@angular/build": "^21.2.20",
"@angular/cli": "^21.2.20",
"@angular/compiler-cli": "^21.2.19",
"@commitlint/cli": "^20.4.3",
"@commitlint/config-conventional": "^20.4.3",
"@angular-eslint/eslint-plugin": "^21.4.0",
"@angular-eslint/eslint-plugin-template": "^21.4.0",
"@angular-eslint/template-parser": "^21.4.0",
"@angular/build": "^21.2.24",
"@angular/cli": "^21.2.24",
"@angular/compiler-cli": "^21.2.25",
"@commitlint/cli": "^20.5.3",
"@commitlint/config-conventional": "^20.5.3",
"@eslint/js": "^10.0.1",
"@types/express": "^5.0.1",
"@types/gapi": "^0.0.47",
"@types/gapi.auth2": "^0.0.61",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.17.19",
"@vitest/coverage-v8": "^4.1.9",
"angular-eslint": "^21.3.0",
"@vitest/coverage-v8": "^4.1.11",
"angular-eslint": "^21.4.0",
"angularx-qrcode": "^21.0.4",
"eslint": "^10.0.2",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.4.1",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^16.3.2",
"ng-mocks": "^14.15.3",
"ng-mocks": "^14.18.1",
"prettier": "3.8.1",
"source-map-explorer": "^2.5.3",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"vitest": "^4.1.9"
"typescript-eslint": "^8.71.0",
"vitest": "^4.1.11"
},
"overrides": {
"piscina": "5.3.2"
},
"lint-staged": {
"**/*.{ts,html,scss}": [
Expand Down
4 changes: 4 additions & 0 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
<section class="maintenance-overlay font-bold text-xl flex flex-column align-items-center justify-content-center">
<p>{{ 'maintenance.title' | translate }}</p>
<p>{{ 'maintenance.message' | translate }}</p>
<p>
{{ 'maintenance.details' | translate }}
<a href="https://status.cos.io/" target="_blank" rel="noopener noreferrer">https://status.cos.io/</a>
</p>
</section>
}
2 changes: 1 addition & 1 deletion src/app/app.component.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.maintenance-overlay {
position: fixed;
inset: 0;
z-index: 2000;
z-index: 3100;
background: var(--white);
}
8 changes: 8 additions & 0 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export const routes: Routes = [
(mod) => mod.ChooseRepositoryComponent
),
},
{
path: 'resend',
loadComponent: () =>
import('./features/auth/pages/resend/resend-confirmation.component').then(
(mod) => mod.ResendConfirmationComponent
),
data: { skipBreadcrumbs: true },
},
{
path: 'search',
loadComponent: () => import('./features/search/search.component').then((mod) => mod.SearchComponent),
Expand Down
5 changes: 5 additions & 0 deletions src/app/core/enums/maintenance-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum MaintenanceStatus {
Active = 'active',
Inactive = 'inactive',
Unknown = 'unknown',
}
104 changes: 104 additions & 0 deletions src/app/core/services/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { CookieService } from 'ngx-cookie-service';
import { MockProvider } from 'ng-mocks';

import { PLATFORM_ID } from '@angular/core';
import { TestBed } from '@angular/core/testing';

import { ENVIRONMENT } from '@core/provider/environment.provider';
import { WINDOW } from '@core/provider/window.provider';
import { JsonApiService } from '@osf/shared/services/json-api.service';
import { LoaderService } from '@osf/shared/services/loader.service';

import { CookieServiceMock } from '@testing/providers/cookie-service.mock';
import { JsonApiServiceMock } from '@testing/providers/json-api.service.mock';
import { LoaderServiceMock } from '@testing/providers/loader-service.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';

import { AuthService } from './auth.service';

describe('AuthService', () => {
const webUrl = 'https://web.test';
const casUrl = 'https://cas.test';
const origin = 'https://osf.test';

let service: AuthService;
let loaderService: LoaderServiceMock;
let locationMock: { href: string; pathname: string; origin: string };

function setup(overrides: { pathname?: string; href?: string; isBrowser?: boolean } = {}) {
const pathname = overrides.pathname ?? '/dashboard';
locationMock = {
href: overrides.href ?? `${origin}${pathname}`,
pathname,
origin,
};

loaderService = new LoaderServiceMock();

TestBed.configureTestingModule({
providers: [
AuthService,
provideMockStore(),
MockProvider(JsonApiService, JsonApiServiceMock.simple()),
MockProvider(CookieService, CookieServiceMock.simple()),
MockProvider(LoaderService, loaderService),
MockProvider(WINDOW, { location: locationMock } as Window),
MockProvider(ENVIRONMENT, { webUrl, casUrl, apiDomainUrl: 'https://api.test' }),
MockProvider(PLATFORM_ID, overrides.isBrowser === false ? 'server' : 'browser'),
],
});

service = TestBed.inject(AuthService);
}

function nextFromServiceParam(href: string): string | null {
const serviceParam = new URL(href).searchParams.get('service');
return serviceParam ? new URL(serviceParam).searchParams.get('next') : null;
}

it('should use the current page as next when signing in from a regular page', () => {
const href = `${origin}/project/abc`;
setup({ pathname: '/project/abc', href });

service.navigateToSignIn();

expect(loaderService.show).toHaveBeenCalled();
expect(nextFromServiceParam(locationMock.href)).toBe(href);
});

it('should use home as next when signing in from reset password', () => {
setup({
pathname: '/resetpassword/user-1/token-1',
href: `${origin}/resetpassword/user-1/token-1`,
});

service.navigateToSignIn();

expect(nextFromServiceParam(locationMock.href)).toBe(`${webUrl}/`);
});

it('should use home as next when signing in from forgot password with orcid', () => {
setup({ pathname: '/forgotpassword', href: `${origin}/forgotpassword` });

service.navigateToOrcidSignIn();

expect(new URL(locationMock.href).searchParams.get('next')).toBe(`${webUrl}/`);
});

it('should use home as next when signing in from register with institution', () => {
setup({ pathname: '/register', href: `${origin}/register` });

service.navigateToInstitutionSignIn();

expect(new URL(locationMock.href).searchParams.get('next')).toBe(`${webUrl}/`);
});

it('should not redirect to sign in when not in the browser', () => {
setup({ isBrowser: false, href: `${origin}/dashboard` });

service.navigateToSignIn();

expect(loaderService.show).not.toHaveBeenCalled();
expect(locationMock.href).toBe(`${origin}/dashboard`);
});
});
40 changes: 33 additions & 7 deletions src/app/core/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { inject, Injectable, PLATFORM_ID } from '@angular/core';

import { SignUpModel } from '@core/models/sign-up.model';
import { ENVIRONMENT } from '@core/provider/environment.provider';
import { WINDOW } from '@core/provider/window.provider';
import { ClearCurrentUser } from '@osf/core/store/user';
import { urlParam } from '@osf/shared/helpers/url-param.helper';
import { JsonApiService } from '@osf/shared/services/json-api.service';
Expand All @@ -20,6 +21,7 @@ export class AuthService {
private readonly cookieService = inject(CookieService);
private readonly loaderService = inject(LoaderService);
private readonly environment = inject(ENVIRONMENT);
private readonly window = inject(WINDOW);
private readonly platformId = inject(PLATFORM_ID);
private readonly actions = createDispatchMap({ clearCurrentUser: ClearCurrentUser });

Expand All @@ -43,12 +45,12 @@ export class AuthService {
this.loaderService.show();

const serviceUrl = new URL(`${this.webUrl}/login`);
serviceUrl.searchParams.set('next', window.location.href);
serviceUrl.searchParams.set('next', this.getPostLoginRedirectUrl());

const loginUrl = new URL(`${this.casUrl}/login`);
loginUrl.searchParams.set('service', serviceUrl.toString());

window.location.href = loginUrl.toString();
this.window.location.href = loginUrl.toString();
}

navigateToOrcidSignIn(): void {
Expand All @@ -59,10 +61,10 @@ export class AuthService {
const loginUrl = `${this.casUrl}/login?${urlParam({
redirectOrcid: 'true',
service: `${this.webUrl}/login`,
next: window.location.href,
next: this.getPostLoginRedirectUrl(),
})}`;

window.location.href = loginUrl;
this.window.location.href = loginUrl;
}

navigateToInstitutionSignIn(): void {
Expand All @@ -73,10 +75,10 @@ export class AuthService {
const loginUrl = `${this.casUrl}/login?${urlParam({
campaign: 'institution',
service: `${this.webUrl}/login`,
next: window.location.href,
next: this.getPostLoginRedirectUrl(),
})}`;

window.location.href = loginUrl;
this.window.location.href = loginUrl;
}

logout(nextUrl?: string): void {
Expand All @@ -85,7 +87,7 @@ export class AuthService {

if (isPlatformBrowser(this.platformId)) {
this.cookieService.deleteAll();
window.location.href = `${this.webUrl}/logout/?next=${encodeURIComponent(nextUrl || `${window.location.origin}/`)}`;
this.window.location.href = `${this.webUrl}/logout/?next=${encodeURIComponent(nextUrl || `${this.window.location.origin}/`)}`;
}
}

Expand Down Expand Up @@ -117,4 +119,28 @@ export class AuthService {

return this.jsonApiService.post(baseUrl, body);
}

resendConfirmationUrl(email: string) {
const baseUrl = `${this.apiUrl}/resend_confirmation/`;
const body = {
data: {
attributes: {
email: email,
},
},
};

return this.jsonApiService.post(baseUrl, body);
}

private getPostLoginRedirectUrl(): string {
const pathname = this.window.location.pathname;
const excludedPaths = ['/resetpassword', '/forgotpassword', '/register', '/resend'];

if (excludedPaths.some((path) => pathname === path || pathname.startsWith(`${path}/`))) {
return `${this.webUrl}/`;
}

return this.window.location.href;
}
}
Loading
Loading