From 996e8b75e0a43768378c8b23a8b7c6f36b2f01fa Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Mon, 12 May 2025 19:27:02 +0300 Subject: [PATCH 1/4] chore(account-settings): api integration --- package.json | 1 + src/app/app.routes.ts | 8 +- .../core/constants/ngxs-states.constant.ts | 2 + .../services/json-api/json-api.service.ts | 6 +- .../underscore-entites/user/user-us.entity.ts | 9 +- .../services/mappers/users/users.mapper.ts | 2 + src/app/core/services/user/user.entity.ts | 2 + .../confirm-email.component.html | 14 + .../confirm-email.component.scss | 6 + .../confirm-email.component.spec.ts | 22 ++ .../confirm-email/confirm-email.component.ts | 29 ++ .../home/{ => components}/logged-out/data.ts | 0 .../logged-out/home-logged-out.component.html | 98 +++++-- .../logged-out/home-logged-out.component.scss | 2 +- .../home-logged-out.component.spec.ts | 0 .../logged-out/home-logged-out.component.ts | 0 src/app/features/home/home.component.ts | 42 ++- .../institutions/institutions.service.ts | 9 + .../account-settings.component.html | 197 +------------ .../account-settings.component.scss | 12 +- .../account-settings.component.ts | 95 +++--- .../account-settings.const.ts | 15 - .../account-settings.route.ts | 3 - .../add-email/add-email.component.html | 4 +- .../add-email/add-email.component.scss | 0 .../add-email/add-email.component.spec.ts | 0 .../add-email/add-email.component.ts | 12 + .../affiliated-institutions.component.html | 23 ++ .../affiliated-institutions.component.scss | 57 ++++ .../affiliated-institutions.component.spec.ts | 22 ++ .../affiliated-institutions.component.ts | 26 ++ .../change-password.component.html | 91 ++++++ .../change-password.component.scss | 16 ++ .../change-password.component.spec.ts | 22 ++ .../change-password.component.ts | 117 ++++++++ .../connected-emails.component.html | 104 +++++++ .../connected-emails.component.scss | 98 +++++++ .../connected-emails.component.spec.ts | 22 ++ .../connected-emails.component.ts | 83 ++++++ .../connected-identities.component.html | 25 ++ .../connected-identities.component.scss | 5 + .../connected-identities.component.spec.ts | 22 ++ .../connected-identities.component.ts | 22 ++ .../cancel-deactivation.component.html | 16 ++ .../cancel-deactivation.component.scss | 7 + .../cancel-deactivation.component.spec.ts | 22 ++ .../cancel-deactivation.component.ts | 25 ++ .../deactivation-warning.component.html | 16 ++ .../deactivation-warning.component.scss | 7 + .../deactivation-warning.component.spec.ts | 22 ++ .../deactivation-warning.component.ts | 25 ++ .../deactivate-account.component.html | 30 ++ .../deactivate-account.component.scss | 12 + .../deactivate-account.component.spec.ts | 0 .../deactivate-account.component.ts | 48 ++++ .../default-storage-location.component.html | 22 ++ .../default-storage-location.component.scss | 5 + ...default-storage-location.component.spec.ts | 22 ++ .../default-storage-location.component.ts | 47 +++ .../account-settings/components/index.ts | 9 + .../share-indexing.component.html | 29 ++ .../share-indexing.component.scss | 5 + .../share-indexing.component.spec.ts | 22 ++ .../share-indexing.component.ts | 44 +++ .../configure-two-factor.component.html | 17 ++ .../configure-two-factor.component.scss | 7 + .../configure-two-factor.component.spec.ts | 22 ++ .../configure-two-factor.component.ts | 30 ++ .../verify-two-factor.component.html | 10 + .../verify-two-factor.component.scss | 7 + .../verify-two-factor.component.spec.ts | 22 ++ .../verify-two-factor.component.ts | 26 ++ .../two-factor-auth.component.html | 62 ++++ .../two-factor-auth.component.scss | 16 ++ .../two-factor-auth.component.spec.ts | 22 ++ .../two-factor-auth.component.ts | 92 ++++++ .../deactivate-account.component.html | 9 - .../deactivate-account.component.scss | 0 .../deactivate-account.component.ts | 12 - .../mappers/account-settings.mapper.ts | 15 + .../account-settings/mappers/emails.mapper.ts | 22 ++ .../mappers/external-identities.mapper.ts | 15 + .../mappers/regions.mapper.ts | 18 ++ .../osf-entities/account-email.entity.ts | 8 + .../osf-entities/account-settings.entity.ts | 9 + .../external-institution.entity.ts | 5 + .../models/osf-entities/region.entity.ts | 4 + .../get-account-settings-response.entity.ts | 13 + .../responses/get-email-response.entity.ts | 4 + .../responses/get-regions-response.entity.ts | 4 + .../models/responses/list-emails.entity.ts | 11 + .../list-identities-response.entity.ts | 8 + .../services/account-settings.service.ts | 232 +++++++++++++++ .../store/account-settings.actions.ts | 109 +++++++ .../store/account-settings.model.ts | 14 + .../store/account-settings.selectors.ts | 51 ++++ .../store/account-settings.state.ts | 271 ++++++++++++++++++ src/app/features/settings/index.ts | 1 - src/assets/icons/source/warning-sign.svg | 6 +- src/assets/styles/overrides/message.scss | 14 + src/assets/styles/overrides/password.scss | 7 + src/assets/styles/overrides/spinner.scss | 5 + src/assets/styles/styles.scss | 3 + 103 files changed, 2670 insertions(+), 313 deletions(-) create mode 100644 src/app/features/home/components/confirm-email/confirm-email.component.html create mode 100644 src/app/features/home/components/confirm-email/confirm-email.component.scss create mode 100644 src/app/features/home/components/confirm-email/confirm-email.component.spec.ts create mode 100644 src/app/features/home/components/confirm-email/confirm-email.component.ts rename src/app/features/home/{ => components}/logged-out/data.ts (100%) rename src/app/features/home/{ => components}/logged-out/home-logged-out.component.html (68%) rename src/app/features/home/{ => components}/logged-out/home-logged-out.component.scss (99%) rename src/app/features/home/{ => components}/logged-out/home-logged-out.component.spec.ts (100%) rename src/app/features/home/{ => components}/logged-out/home-logged-out.component.ts (100%) delete mode 100644 src/app/features/settings/account-settings/account-settings.const.ts delete mode 100644 src/app/features/settings/account-settings/account-settings.route.ts rename src/app/features/settings/account-settings/{ => components}/add-email/add-email.component.html (86%) rename src/app/features/settings/account-settings/{ => components}/add-email/add-email.component.scss (100%) rename src/app/features/settings/account-settings/{ => components}/add-email/add-email.component.spec.ts (100%) rename src/app/features/settings/account-settings/{ => components}/add-email/add-email.component.ts (68%) create mode 100644 src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html create mode 100644 src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss create mode 100644 src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts create mode 100644 src/app/features/settings/account-settings/components/change-password/change-password.component.html create mode 100644 src/app/features/settings/account-settings/components/change-password/change-password.component.scss create mode 100644 src/app/features/settings/account-settings/components/change-password/change-password.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/change-password/change-password.component.ts create mode 100644 src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html create mode 100644 src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.scss create mode 100644 src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts create mode 100644 src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html create mode 100644 src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.scss create mode 100644 src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.html create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.scss create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.ts create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.html create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.scss create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.ts create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss rename src/app/features/settings/account-settings/{deactivate-account => components}/deactivate-account/deactivate-account.component.spec.ts (100%) create mode 100644 src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts create mode 100644 src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html create mode 100644 src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.scss create mode 100644 src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts create mode 100644 src/app/features/settings/account-settings/components/index.ts create mode 100644 src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html create mode 100644 src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.scss create mode 100644 src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.spec.ts create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts delete mode 100644 src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.html delete mode 100644 src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.scss delete mode 100644 src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.ts create mode 100644 src/app/features/settings/account-settings/mappers/account-settings.mapper.ts create mode 100644 src/app/features/settings/account-settings/mappers/emails.mapper.ts create mode 100644 src/app/features/settings/account-settings/mappers/external-identities.mapper.ts create mode 100644 src/app/features/settings/account-settings/mappers/regions.mapper.ts create mode 100644 src/app/features/settings/account-settings/models/osf-entities/account-email.entity.ts create mode 100644 src/app/features/settings/account-settings/models/osf-entities/account-settings.entity.ts create mode 100644 src/app/features/settings/account-settings/models/osf-entities/external-institution.entity.ts create mode 100644 src/app/features/settings/account-settings/models/osf-entities/region.entity.ts create mode 100644 src/app/features/settings/account-settings/models/responses/get-account-settings-response.entity.ts create mode 100644 src/app/features/settings/account-settings/models/responses/get-email-response.entity.ts create mode 100644 src/app/features/settings/account-settings/models/responses/get-regions-response.entity.ts create mode 100644 src/app/features/settings/account-settings/models/responses/list-emails.entity.ts create mode 100644 src/app/features/settings/account-settings/models/responses/list-identities-response.entity.ts create mode 100644 src/app/features/settings/account-settings/services/account-settings.service.ts create mode 100644 src/app/features/settings/account-settings/store/account-settings.actions.ts create mode 100644 src/app/features/settings/account-settings/store/account-settings.model.ts create mode 100644 src/app/features/settings/account-settings/store/account-settings.selectors.ts create mode 100644 src/app/features/settings/account-settings/store/account-settings.state.ts create mode 100644 src/assets/styles/overrides/password.scss create mode 100644 src/assets/styles/overrides/spinner.scss diff --git a/package.json b/package.json index 75592ea3c..f27b09edb 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@types/jasmine": "~5.1.0", "@types/jest": "^29.5.14", "angular-eslint": "19.1.0", + "angularx-qrcode": "^19.0.0", "eslint": "^9.20.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-import": "^2.31.0", diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 6b7fd219c..c7b266930 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -38,7 +38,9 @@ export const routes: Routes = [ { path: 'home-logged-out', loadComponent: () => - import('./features/home/logged-out/home-logged-out.component').then((mod) => mod.HomeLoggedOutComponent), + import('@osf/features/home/components/logged-out/home-logged-out.component').then( + (mod) => mod.HomeLoggedOutComponent + ), }, { path: 'support', @@ -117,6 +119,10 @@ export const routes: Routes = [ loadComponent: () => import('./features/my-profile/my-profile.component').then((mod) => mod.MyProfileComponent), providers: [provideStates([ResourceFiltersState, ResourceFiltersOptionsState])], }, + { + path: 'confirm/:userId/:emailId', + loadComponent: () => import('./features/home/home.component').then((mod) => mod.HomeComponent), + }, ], }, ]; diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 279b085b9..93ad98ce6 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -3,6 +3,7 @@ import { UserState } from '@core/store/user'; import { InstitutionsState } from '@osf/features/institutions/store'; import { MyProjectsState } from '@osf/features/my-projects/store'; import { SearchState } from '@osf/features/search/store'; +import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; import { AddonsState } from '@osf/features/settings/addons/store'; import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store'; import { ProfileSettingsState } from '@osf/features/settings/profile-settings/profile-settings.state'; @@ -18,4 +19,5 @@ export const STATES = [ InstitutionsState, ProfileSettingsState, DeveloperAppsState, + AccountSettingsState, ]; diff --git a/src/app/core/services/json-api/json-api.service.ts b/src/app/core/services/json-api/json-api.service.ts index ea16cdf27..3a70ba532 100644 --- a/src/app/core/services/json-api/json-api.service.ts +++ b/src/app/core/services/json-api/json-api.service.ts @@ -11,6 +11,8 @@ import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; export class JsonApiService { http: HttpClient = inject(HttpClient); readonly #token = 'Bearer 2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt'; + // OBJoUomBgbUuDgQo5JoaSKNya6XaYcd0ojAX1XOLmWi6J2arQPzByxyEi81fHE60drQUWv + // UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm readonly #headers = new HttpHeaders({ Authorization: this.#token, Accept: 'application/vnd.api+json', @@ -65,7 +67,7 @@ export class JsonApiService { .pipe(map((response) => response.data)); } - delete(url: string): Observable { - return this.http.delete(url, { headers: this.#headers }); + delete(url: string, body?: unknown): Observable { + return this.http.delete(url, { headers: this.#headers, body: body }); } } diff --git a/src/app/core/services/json-api/underscore-entites/user/user-us.entity.ts b/src/app/core/services/json-api/underscore-entites/user/user-us.entity.ts index efb16cd2b..4e1942c0f 100644 --- a/src/app/core/services/json-api/underscore-entites/user/user-us.entity.ts +++ b/src/app/core/services/json-api/underscore-entites/user/user-us.entity.ts @@ -16,8 +16,15 @@ export interface UserUS { suffix?: string; social: Social; date_registered: string; + allow_indexing?: boolean; + }; + relationships: { + default_region: { + data: { + id: string; + }; + }; }; - relationships: Record; links: { html: string; profile_image: string; diff --git a/src/app/core/services/mappers/users/users.mapper.ts b/src/app/core/services/mappers/users/users.mapper.ts index 0cde5c429..bea7973ae 100644 --- a/src/app/core/services/mappers/users/users.mapper.ts +++ b/src/app/core/services/mappers/users/users.mapper.ts @@ -16,5 +16,7 @@ export function mapUserUStoUser(user: UserUS): User { employment: user.attributes.employment, iri: user.links.iri, social: user.attributes.social, + defaultRegionId: user.relationships?.default_region?.data?.id, + allowIndexing: user.attributes?.allow_indexing, }; } diff --git a/src/app/core/services/user/user.entity.ts b/src/app/core/services/user/user.entity.ts index 47ffc35a4..e90d83eb5 100644 --- a/src/app/core/services/user/user.entity.ts +++ b/src/app/core/services/user/user.entity.ts @@ -25,4 +25,6 @@ export interface User { impactStory?: string; researcherId?: string; }; + defaultRegionId: string; + allowIndexing: boolean | undefined; } diff --git a/src/app/features/home/components/confirm-email/confirm-email.component.html b/src/app/features/home/components/confirm-email/confirm-email.component.html new file mode 100644 index 000000000..e7140431f --- /dev/null +++ b/src/app/features/home/components/confirm-email/confirm-email.component.html @@ -0,0 +1,14 @@ +
+

+ Would you like to add +

{{ config.data.emailAddress }}

+ to your account? +

+
+ + Do not add email + + + Add Email +
+
diff --git a/src/app/features/home/components/confirm-email/confirm-email.component.scss b/src/app/features/home/components/confirm-email/confirm-email.component.scss new file mode 100644 index 000000000..cdebc47a6 --- /dev/null +++ b/src/app/features/home/components/confirm-email/confirm-email.component.scss @@ -0,0 +1,6 @@ +:host { + h3, + p { + text-transform: none; + } +} diff --git a/src/app/features/home/components/confirm-email/confirm-email.component.spec.ts b/src/app/features/home/components/confirm-email/confirm-email.component.spec.ts new file mode 100644 index 000000000..d0c783e86 --- /dev/null +++ b/src/app/features/home/components/confirm-email/confirm-email.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfirmEmailComponent } from './confirm-email.component'; + +describe('ConfirmEmailComponent', () => { + let component: ConfirmEmailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfirmEmailComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ConfirmEmailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/home/components/confirm-email/confirm-email.component.ts b/src/app/features/home/components/confirm-email/confirm-email.component.ts new file mode 100644 index 000000000..0303e8fc9 --- /dev/null +++ b/src/app/features/home/components/confirm-email/confirm-email.component.ts @@ -0,0 +1,29 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; + +import { VerifyEmail } from '@osf/features/settings/account-settings/store/account-settings.actions'; + +@Component({ + selector: 'osf-confirm-email', + imports: [Button, FormsModule], + templateUrl: './confirm-email.component.html', + styleUrl: './confirm-email.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConfirmEmailComponent { + readonly #store = inject(Store); + readonly dialogRef = inject(DynamicDialogRef); + readonly config = inject(DynamicDialogConfig); + readonly #router = inject(Router); + + verifyEmail() { + this.#store.dispatch(new VerifyEmail(this.config.data.userId, this.config.data.emailId)); + this.#router.navigate(['/settings/account']); + } +} diff --git a/src/app/features/home/logged-out/data.ts b/src/app/features/home/components/logged-out/data.ts similarity index 100% rename from src/app/features/home/logged-out/data.ts rename to src/app/features/home/components/logged-out/data.ts diff --git a/src/app/features/home/logged-out/home-logged-out.component.html b/src/app/features/home/components/logged-out/home-logged-out.component.html similarity index 68% rename from src/app/features/home/logged-out/home-logged-out.component.html rename to src/app/features/home/components/logged-out/home-logged-out.component.html index bcff11b65..b643a6807 100644 --- a/src/app/features/home/logged-out/home-logged-out.component.html +++ b/src/app/features/home/components/logged-out/home-logged-out.component.html @@ -13,7 +13,12 @@

- better-research + better-research
@@ -40,8 +45,8 @@

- search icon - search icon + search icon + search icon

@@ -55,8 +60,8 @@

- search icon - search icon + search icon + search icon

@@ -70,8 +75,8 @@

- search icon - search icon + search icon + search icon

@@ -85,7 +90,7 @@

- search icon + search icon

@@ -166,28 +171,53 @@

- incommon - sso + incommon + sso
google-scholar - orcid + orcid
- mendeley - zotero + mendeley + zotero
- dropbox - google-drive + dropbox + google-drive

@@ -197,8 +227,13 @@

{{ 'home.loggedOut.integrations.categories.authentication' | translate }}

- incommon - sso + incommon + sso
@@ -206,28 +241,43 @@

{{ 'home.loggedOut.integrations.categories.discovery' | translate }}

google-scholar - orcid + orcid

{{ 'home.loggedOut.integrations.categories.references' | translate }}

- mendeley - zotero + mendeley + zotero

{{ 'home.loggedOut.integrations.categories.storage' | translate }}

- dropbox - google-drive + dropbox + google-drive

} @else { diff --git a/src/app/features/home/logged-out/home-logged-out.component.scss b/src/app/features/home/components/logged-out/home-logged-out.component.scss similarity index 99% rename from src/app/features/home/logged-out/home-logged-out.component.scss rename to src/app/features/home/components/logged-out/home-logged-out.component.scss index c2c329b42..4d0c0f215 100644 --- a/src/app/features/home/logged-out/home-logged-out.component.scss +++ b/src/app/features/home/components/logged-out/home-logged-out.component.scss @@ -1,4 +1,4 @@ -@use "assets/styles/variables" as var; +@use "../../../../../assets/styles/variables" as var; :host { .home-container { diff --git a/src/app/features/home/logged-out/home-logged-out.component.spec.ts b/src/app/features/home/components/logged-out/home-logged-out.component.spec.ts similarity index 100% rename from src/app/features/home/logged-out/home-logged-out.component.spec.ts rename to src/app/features/home/components/logged-out/home-logged-out.component.spec.ts diff --git a/src/app/features/home/logged-out/home-logged-out.component.ts b/src/app/features/home/components/logged-out/home-logged-out.component.ts similarity index 100% rename from src/app/features/home/logged-out/home-logged-out.component.ts rename to src/app/features/home/components/logged-out/home-logged-out.component.ts diff --git a/src/app/features/home/home.component.ts b/src/app/features/home/home.component.ts index 3e903035d..c462456a8 100644 --- a/src/app/features/home/home.component.ts +++ b/src/app/features/home/home.component.ts @@ -4,20 +4,22 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { SortEvent } from 'primeng/api'; import { Button } from 'primeng/button'; -import { DialogService } from 'primeng/dynamicdialog'; +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; import { TablePageEvent } from 'primeng/table'; -import { debounceTime, distinctUntilChanged, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, Subject, take } from 'rxjs'; import { Component, computed, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { MY_PROJECTS_TABLE_PARAMS } from '@core/constants/my-projects-table.constants'; +import { ConfirmEmailComponent } from '@osf/features/home/components/confirm-email/confirm-email.component'; import { GetUserInstitutions } from '@osf/features/institutions/store'; import { MyProjectsItem } from '@osf/features/my-projects/entities/my-projects.entities'; import { MyProjectsSearchFilters } from '@osf/features/my-projects/entities/my-projects-search-filters.models'; import { ClearMyProjects, GetMyProjects, MyProjectsSelectors } from '@osf/features/my-projects/store'; +import { AccountSettingsService } from '@osf/features/settings/account-settings/services/account-settings.service'; import { AddProjectFormComponent } from '@shared/components/add-project-form/add-project-form.component'; import { MyProjectsTableComponent } from '@shared/components/my-projects-table/my-projects-table.component'; import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; @@ -43,6 +45,7 @@ export class HomeComponent implements OnInit { readonly #isXSmall$ = inject(IS_XSMALL); readonly #isMedium$ = inject(IS_MEDIUM); readonly #searchSubject = new Subject(); + readonly #accountSettingsServer = inject(AccountSettingsService); protected readonly isLoading = signal(false); protected readonly isSubmitting = signal(false); @@ -66,6 +69,9 @@ export class HomeComponent implements OnInit { return this.projects().filter((project) => project.title.toLowerCase().includes(search)); }); + dialogRef: DynamicDialogRef | null = null; + emailAddress = ''; + constructor() { this.#setupSearchSubscription(); this.#setupTotalRecordsEffect(); @@ -75,6 +81,38 @@ export class HomeComponent implements OnInit { ngOnInit() { this.#setupQueryParamsSubscription(); this.#store.dispatch(new GetUserInstitutions()); + + // Check for userId and emailId route parameters + this.#route.params.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((params) => { + const userId = params['userId']; + const emailId = params['emailId']; + + if (userId && emailId) { + this.#accountSettingsServer + .getEmail(emailId, userId) + .pipe(take(1)) + .subscribe((email) => { + this.emailAddress = email.emailAddress; + this.addAlternateEmail(); + }); + } + }); + } + + addAlternateEmail() { + this.dialogRef = this.#dialogService.open(ConfirmEmailComponent, { + width: '448px', + focusOnShow: false, + header: 'Add alternative email', + closeOnEscape: true, + modal: true, + closable: true, + data: { + emailAddress: this.emailAddress, + userId: this.#route.snapshot.params['userId'], + emailId: this.#route.snapshot.params['emailId'], + }, + }); } #setupQueryParamsSubscription(): void { diff --git a/src/app/features/institutions/institutions.service.ts b/src/app/features/institutions/institutions.service.ts index 72f93fe97..58f498981 100644 --- a/src/app/features/institutions/institutions.service.ts +++ b/src/app/features/institutions/institutions.service.ts @@ -6,6 +6,8 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { environment } from '../../../environments/environment'; + import { Institution, UserInstitutionGetResponse } from './entities/institutions.models'; import { InstitutionsMapper } from './mappers/institutions.mapper'; @@ -23,4 +25,11 @@ export class InstitutionsService { .get>(url) .pipe(map((response) => response.data.map((item) => InstitutionsMapper.fromResponse(item)))); } + + deleteUserInstitution(id: string, userId: string): Observable { + const payload = { + data: [{ id: id, type: 'institutions' }], + }; + return this.#jsonApiService.delete(`${environment.apiUrl}/users/${userId}/relationships/institutions/`, payload); + } } diff --git a/src/app/features/settings/account-settings/account-settings.component.html b/src/app/features/settings/account-settings/account-settings.component.html index 3876b9ede..5f49e314f 100644 --- a/src/app/features/settings/account-settings/account-settings.component.html +++ b/src/app/features/settings/account-settings/account-settings.component.html @@ -1,196 +1,21 @@ -
- +} diff --git a/src/app/features/settings/account-settings/account-settings.component.scss b/src/app/features/settings/account-settings/account-settings.component.scss index db8c5e9db..9ec01d6c2 100644 --- a/src/app/features/settings/account-settings/account-settings.component.scss +++ b/src/app/features/settings/account-settings/account-settings.component.scss @@ -85,7 +85,11 @@ &-email { display: flex; gap: 2rem; - align-items: center; + align-items: start; + + &__title { + min-width: 10rem; + } &--readonly { display: flex; @@ -93,6 +97,12 @@ border: 1px solid var.$grey-2; padding: 0.285rem 0.85rem; border-radius: 0.285rem; + + i { + color: var.$dark-blue-1; + font-size: 0.7rem; + margin-left: 0.7rem; + } } &__value { diff --git a/src/app/features/settings/account-settings/account-settings.component.ts b/src/app/features/settings/account-settings/account-settings.component.ts index fa895f8c6..245f8c22a 100644 --- a/src/app/features/settings/account-settings/account-settings.component.ts +++ b/src/app/features/settings/account-settings/account-settings.component.ts @@ -1,70 +1,65 @@ -import { Button } from 'primeng/button'; -import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { InputText } from 'primeng/inputtext'; -import { Message } from 'primeng/message'; -import { RadioButton } from 'primeng/radiobutton'; -import { Select } from 'primeng/select'; +import { Store } from '@ngxs/store'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { DialogService } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; +import { UserSelectors } from '@core/store/user/user.selectors'; +import { + AffiliatedInstitutionsComponent, + ChangePasswordComponent, + ConnectedEmailsComponent, + ConnectedIdentitiesComponent, + DeactivateAccountComponent, + DefaultStorageLocationComponent, + ShareIndexingComponent, + TwoFactorAuthComponent, +} from '@osf/features/settings/account-settings/components'; import { - AccountSettingsPasswordForm, - AccountSettingsPasswordFormControls, -} from '@osf/features/settings/account-settings/account.settings.entities'; -import { MOCK_COUNTRIES } from '@osf/features/settings/account-settings/account-settings.const'; -import { AddEmailComponent } from '@osf/features/settings/account-settings/add-email/add-email.component'; -import { DeactivateAccountComponent } from '@osf/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component'; + GetAccountSettings, + GetEmails, + GetExternalIdentities, + GetRegions, + GetUserInstitutions, +} from '@osf/features/settings/account-settings/store/account-settings.actions'; import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; @Component({ selector: 'osf-account-settings', - imports: [Button, Select, RadioButton, ReactiveFormsModule, InputText, Message, SubHeaderComponent], + imports: [ + ReactiveFormsModule, + SubHeaderComponent, + ConnectedEmailsComponent, + DefaultStorageLocationComponent, + ConnectedIdentitiesComponent, + ShareIndexingComponent, + ChangePasswordComponent, + TwoFactorAuthComponent, + DeactivateAccountComponent, + AffiliatedInstitutionsComponent, + ], providers: [DialogService], templateUrl: './account-settings.component.html', styleUrl: './account-settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AccountSettingsComponent { + readonly #store = inject(Store); protected readonly isMobile = toSignal(inject(IS_XSMALL)); - readonly passwordForm: AccountSettingsPasswordForm = new FormGroup({ - [AccountSettingsPasswordFormControls.OldPassword]: new FormControl('', { - nonNullable: true, - }), - [AccountSettingsPasswordFormControls.NewPassword]: new FormControl('', { - nonNullable: true, - }), - [AccountSettingsPasswordFormControls.ConfirmPassword]: new FormControl('', { - nonNullable: true, - }), - }); - protected readonly optControl = new FormControl(false); - protected readonly MOCK_COUNTRIES = MOCK_COUNTRIES; - protected readonly AccountSettingsPasswordFormControls = AccountSettingsPasswordFormControls; - private readonly dialogService = inject(DialogService); - private dialogRef: DynamicDialogRef | null = null; - - addEmail() { - this.dialogRef = this.dialogService.open(AddEmailComponent, { - width: '448px', - focusOnShow: false, - header: 'Add alternative email', - closeOnEscape: true, - modal: true, - closable: true, - }); - } + protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); - deactivateAccount() { - this.dialogRef = this.dialogService.open(DeactivateAccountComponent, { - width: '448px', - focusOnShow: false, - header: 'Deactivate account', - closeOnEscape: true, - modal: true, - closable: true, + constructor() { + effect(() => { + if (this.currentUser()) { + this.#store.dispatch(GetAccountSettings); + this.#store.dispatch(GetEmails); + this.#store.dispatch(GetExternalIdentities); + this.#store.dispatch(GetRegions); + this.#store.dispatch(GetUserInstitutions); + } }); } } diff --git a/src/app/features/settings/account-settings/account-settings.const.ts b/src/app/features/settings/account-settings/account-settings.const.ts deleted file mode 100644 index 658c0efd8..000000000 --- a/src/app/features/settings/account-settings/account-settings.const.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface Country { - name: string; - code: string; -} - -export const MOCK_COUNTRIES: Country[] = [ - { - name: 'United States', - code: 'US', - }, - { - name: 'Canada', - code: 'CA', - }, -]; diff --git a/src/app/features/settings/account-settings/account-settings.route.ts b/src/app/features/settings/account-settings/account-settings.route.ts deleted file mode 100644 index c9de1f6f8..000000000 --- a/src/app/features/settings/account-settings/account-settings.route.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Routes } from '@angular/router'; - -export const accountSettingsRoute: Routes[] = []; diff --git a/src/app/features/settings/account-settings/add-email/add-email.component.html b/src/app/features/settings/account-settings/components/add-email/add-email.component.html similarity index 86% rename from src/app/features/settings/account-settings/add-email/add-email.component.html rename to src/app/features/settings/account-settings/components/add-email/add-email.component.html index 688a68d0e..a8941bc0c 100644 --- a/src/app/features/settings/account-settings/add-email/add-email.component.html +++ b/src/app/features/settings/account-settings/components/add-email/add-email.component.html @@ -9,6 +9,8 @@ Cancel - Add Email + + Add Email +

diff --git a/src/app/features/settings/account-settings/add-email/add-email.component.scss b/src/app/features/settings/account-settings/components/add-email/add-email.component.scss similarity index 100% rename from src/app/features/settings/account-settings/add-email/add-email.component.scss rename to src/app/features/settings/account-settings/components/add-email/add-email.component.scss diff --git a/src/app/features/settings/account-settings/add-email/add-email.component.spec.ts b/src/app/features/settings/account-settings/components/add-email/add-email.component.spec.ts similarity index 100% rename from src/app/features/settings/account-settings/add-email/add-email.component.spec.ts rename to src/app/features/settings/account-settings/components/add-email/add-email.component.spec.ts diff --git a/src/app/features/settings/account-settings/add-email/add-email.component.ts b/src/app/features/settings/account-settings/components/add-email/add-email.component.ts similarity index 68% rename from src/app/features/settings/account-settings/add-email/add-email.component.ts rename to src/app/features/settings/account-settings/components/add-email/add-email.component.ts index 4483daaf9..6482fecc0 100644 --- a/src/app/features/settings/account-settings/add-email/add-email.component.ts +++ b/src/app/features/settings/account-settings/components/add-email/add-email.component.ts @@ -1,3 +1,5 @@ +import { Store } from '@ngxs/store'; + import { Button } from 'primeng/button'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { InputText } from 'primeng/inputtext'; @@ -5,6 +7,8 @@ import { InputText } from 'primeng/inputtext'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { AddEmail } from '@osf/features/settings/account-settings/store/account-settings.actions'; + @Component({ selector: 'osf-add-email', imports: [InputText, ReactiveFormsModule, Button], @@ -13,7 +17,15 @@ import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AddEmailComponent { + readonly #store = inject(Store); readonly dialogRef = inject(DynamicDialogRef); protected readonly emailControl = new FormControl('', [Validators.email, Validators.required]); + + addEmail() { + if (this.emailControl.value) { + this.#store.dispatch(new AddEmail(this.emailControl.value)); + } + this.dialogRef.close(); + } } diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html new file mode 100644 index 000000000..1b68cdf79 --- /dev/null +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html @@ -0,0 +1,23 @@ + diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss new file mode 100644 index 000000000..21819b894 --- /dev/null +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss @@ -0,0 +1,57 @@ +@use "../../account-settings.component.scss" as account-settings; +@use "assets/styles/variables" as var; + +:host { + @extend .account-setting; + + h3, + p { + font-weight: 400; + text-transform: none; + } + + .account-setting { + &-emails { + display: flex; + flex-direction: column; + gap: 1.7rem; + } + + &-email { + display: flex; + gap: 2rem; + align-items: start; + + &--readonly { + display: flex; + align-items: center; + border: 1px solid var.$grey-2; + padding: 0.285rem 0.85rem; + border-radius: 0.285rem; + min-height: 2.8rem; + + &--address { + max-width: 14rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + i { + color: var.$dark-blue-1; + font-size: 0.7rem; + margin-left: 0.7rem; + cursor: pointer; + font-weight: 400; + } + } + + &__value { + display: flex; + align-items: center; + gap: 0.428rem; + min-height: 2.8rem; + } + } + } +} diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.spec.ts b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.spec.ts new file mode 100644 index 000000000..eb12c0470 --- /dev/null +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AffiliatedInstitutionsComponent } from './affiliated-institutions.component'; + +describe('AffiliatedInstitutionsComponent', () => { + let component: AffiliatedInstitutionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AffiliatedInstitutionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AffiliatedInstitutionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts new file mode 100644 index 000000000..cb280a2c7 --- /dev/null +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts @@ -0,0 +1,26 @@ +import { Store } from '@ngxs/store'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { UserSelectors } from '@core/store/user/user.selectors'; +import { DeleteUserInstitution } from '@osf/features/settings/account-settings/store/account-settings.actions'; +import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; + +@Component({ + selector: 'osf-affiliated-institutions', + imports: [], + templateUrl: './affiliated-institutions.component.html', + styleUrl: './affiliated-institutions.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AffiliatedInstitutionsComponent { + readonly #store = inject(Store); + institutions = this.#store.selectSignal(AccountSettingsSelectors.getUserInstitutions); + currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + + deleteInstitution(id: string) { + if (this.currentUser()?.id) { + this.#store.dispatch(new DeleteUserInstitution(id, this.currentUser()!.id)); + } + } +} diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.html b/src/app/features/settings/account-settings/components/change-password/change-password.component.html new file mode 100644 index 000000000..2aa95a0da --- /dev/null +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.html @@ -0,0 +1,91 @@ + diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.scss b/src/app/features/settings/account-settings/components/change-password/change-password.component.scss new file mode 100644 index 000000000..3657ecb76 --- /dev/null +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.scss @@ -0,0 +1,16 @@ +@use "../../account-settings.component.scss" as account-settings; +@use "assets/styles/variables" as var; + +:host { + @extend .account-setting; + + label { + color: var.$dark-blue-1; + } + + .password-help { + color: var.$pr-blue-1; + font-size: 0.8rem; + font-weight: 600; + } +} diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.spec.ts b/src/app/features/settings/account-settings/components/change-password/change-password.component.spec.ts new file mode 100644 index 000000000..2ecf1439e --- /dev/null +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChangePasswordComponent } from './change-password.component'; + +describe('ChangePasswordComponent', () => { + let component: ChangePasswordComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ChangePasswordComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ChangePasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.ts b/src/app/features/settings/account-settings/components/change-password/change-password.component.ts new file mode 100644 index 000000000..8f784b148 --- /dev/null +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.ts @@ -0,0 +1,117 @@ +import { Button } from 'primeng/button'; +import { Password } from 'primeng/password'; + +import { CommonModule } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; +import { + AbstractControl, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; + +import { + AccountSettingsPasswordForm, + AccountSettingsPasswordFormControls, +} from '@osf/features/settings/account-settings/account.settings.entities'; +import { AccountSettingsService } from '@osf/features/settings/account-settings/services/account-settings.service'; + +@Component({ + selector: 'osf-change-password', + imports: [ReactiveFormsModule, Password, CommonModule, Button], + templateUrl: './change-password.component.html', + styleUrl: './change-password.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChangePasswordComponent implements OnInit { + readonly #accountSettingsService = inject(AccountSettingsService); + readonly passwordForm: AccountSettingsPasswordForm = new FormGroup({ + [AccountSettingsPasswordFormControls.OldPassword]: new FormControl('', { + nonNullable: true, + validators: [Validators.required], + }), + [AccountSettingsPasswordFormControls.NewPassword]: new FormControl('', { + nonNullable: true, + validators: [ + Validators.required, + Validators.minLength(8), + Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[\d!@#$%^&*])[A-Za-z\d!@#$%^&*_]{8,}$/), + ], + }), + [AccountSettingsPasswordFormControls.ConfirmPassword]: new FormControl('', { + nonNullable: true, + validators: [Validators.required], + }), + }); + + protected readonly AccountSettingsPasswordFormControls = AccountSettingsPasswordFormControls; + protected errorMessage = signal(''); + + ngOnInit(): void { + // Add form-level validator for password matching and old password check + this.passwordForm.addValidators((control: AbstractControl): ValidationErrors | null => { + const oldPassword = control.get(AccountSettingsPasswordFormControls.OldPassword)?.value; + const newPassword = control.get(AccountSettingsPasswordFormControls.NewPassword)?.value; + const confirmPassword = control.get(AccountSettingsPasswordFormControls.ConfirmPassword)?.value; + + const errors: ValidationErrors = {}; + + // Check if new password matches old password + if (oldPassword && newPassword && oldPassword === newPassword) { + errors['sameAsOldPassword'] = true; + } + + // Check if confirm password matches new password + if (newPassword && confirmPassword && newPassword !== confirmPassword) { + errors['passwordMismatch'] = true; + } + + return Object.keys(errors).length > 0 ? errors : null; + }); + + // Update validation when any password field changes + this.passwordForm.get(AccountSettingsPasswordFormControls.OldPassword)?.valueChanges.subscribe(() => { + this.passwordForm.updateValueAndValidity(); + }); + + this.passwordForm.get(AccountSettingsPasswordFormControls.NewPassword)?.valueChanges.subscribe(() => { + this.passwordForm.updateValueAndValidity(); + }); + + this.passwordForm.get(AccountSettingsPasswordFormControls.ConfirmPassword)?.valueChanges.subscribe(() => { + this.passwordForm.updateValueAndValidity(); + }); + } + + changePassword() { + Object.values(this.passwordForm.controls).forEach((control) => { + control.markAsTouched(); + }); + + if (this.passwordForm.valid) { + this.errorMessage.set(''); + const oldPassword = this.passwordForm.get(AccountSettingsPasswordFormControls.OldPassword)?.value ?? ''; + const newPassword = this.passwordForm.get(AccountSettingsPasswordFormControls.NewPassword)?.value ?? ''; + + this.#accountSettingsService.updatePassword(oldPassword, newPassword).subscribe({ + next: () => { + this.passwordForm.reset(); + Object.values(this.passwordForm.controls).forEach((control) => { + control.markAsUntouched(); + }); + }, + error: (error: HttpErrorResponse) => { + console.error(error); + if (error.error?.errors?.[0]?.detail) { + this.errorMessage.set(error.error.errors[0].detail); + } else { + this.errorMessage.set('Failed to update password. Please try again.'); + } + }, + }); + } + } +} diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html new file mode 100644 index 000000000..568c95b36 --- /dev/null +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html @@ -0,0 +1,104 @@ + diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.scss b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.scss new file mode 100644 index 000000000..7ac7beb37 --- /dev/null +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.scss @@ -0,0 +1,98 @@ +@use "../../account-settings.component.scss" as account-settings; +@use "assets/styles/variables" as var; + +:host { + @extend .account-setting; + + .account-setting { + &-emails { + display: flex; + flex-direction: column; + gap: 1.7rem; + } + + &-email { + display: flex; + gap: 2rem; + align-items: start; + + &-center { + display: flex; + gap: 2rem; + align-items: center; + } + + &__title { + min-width: 10rem; + min-height: 2.8rem; + display: flex; + align-items: center; + } + + &__container { + display: flex; + flex-wrap: wrap; + gap: 2rem; + } + + &--readonly { + display: flex; + align-items: center; + border: 1px solid var.$grey-2; + padding: 0.285rem 0.85rem; + border-radius: 0.285rem; + min-height: 2.8rem; + + &--address { + max-width: 14rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + i { + color: var.$dark-blue-1; + font-size: 0.7rem; + margin-left: 0.7rem; + cursor: pointer; + font-weight: 400; + } + } + + &__value { + display: flex; + align-items: center; + gap: 0.428rem; + min-height: 2.8rem; + } + } + } + + @media (max-width: 600px) { + .account-setting-email { + flex-wrap: wrap; + gap: 0.5rem; + + &__title { + min-height: 2rem; + } + + &__container { + gap: 0.7rem; + } + + &__value { + min-height: 2rem; + } + } + } + + @media (max-width: 410px) { + .account-setting-email { + &__value { + min-height: 2rem; + flex-wrap: wrap; + } + } + } +} diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.spec.ts b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.spec.ts new file mode 100644 index 000000000..0132f7023 --- /dev/null +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConnectedEmailsComponent } from './connected-emails.component'; + +describe('ConnectedEmailsComponent', () => { + let component: ConnectedEmailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConnectedEmailsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ConnectedEmailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts new file mode 100644 index 000000000..c7ef4982f --- /dev/null +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts @@ -0,0 +1,83 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { ProgressSpinner } from 'primeng/progressspinner'; +import { Skeleton } from 'primeng/skeleton'; + +import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; + +import { UserSelectors } from '@core/store/user/user.selectors'; +import { AddEmailComponent } from '@osf/features/settings/account-settings/components'; +import { AccountSettingsService } from '@osf/features/settings/account-settings/services/account-settings.service'; +import { DeleteEmail } from '@osf/features/settings/account-settings/store/account-settings.actions'; +import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; +import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; + +@Component({ + selector: 'osf-connected-emails', + imports: [Button, ProgressSpinner, Skeleton], + templateUrl: './connected-emails.component.html', + styleUrl: './connected-emails.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConnectedEmailsComponent { + readonly #store = inject(Store); + readonly #accountSettingsService = inject(AccountSettingsService); + readonly #dialogService = inject(DialogService); + readonly isMobile = toSignal(inject(IS_XSMALL)); + + protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + protected readonly emails = this.#store.selectSignal(AccountSettingsSelectors.getEmails); + protected readonly deletingEmailIds = signal>(new Set()); + protected readonly unconfirmedEmails = computed(() => { + return this.emails().filter((email) => !email.confirmed && !email.primary); + }); + protected readonly confirmedEmails = computed(() => { + return this.emails().filter((email) => email.confirmed && !email.primary); + }); + protected readonly primaryEmail = computed(() => { + return this.emails().find((email) => email.primary); + }); + protected readonly isEmailsLoading = this.#store.selectSignal(AccountSettingsSelectors.isEmailsLoading); + + dialogRef: DynamicDialogRef | null = null; + + addEmail() { + this.dialogRef = this.#dialogService.open(AddEmailComponent, { + width: '448px', + focusOnShow: false, + header: 'Add alternative email', + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + resendConfirmation(emailId: string) { + if (this.currentUser()?.id) { + this.#accountSettingsService.resendConfirmation(emailId, this.currentUser()!.id).subscribe(); + } + } + + makePrimary(emailId: string) { + if (this.currentUser()?.id) { + this.#accountSettingsService.makePrimary(emailId).subscribe(); + } + } + + deleteEmail(emailId: string) { + const currentDeletingIds = this.deletingEmailIds(); + currentDeletingIds.add(emailId); + this.deletingEmailIds.set(currentDeletingIds); + + this.#store.dispatch(new DeleteEmail(emailId)).subscribe({ + complete: () => { + const updatedDeletingIds = this.deletingEmailIds(); + updatedDeletingIds.delete(emailId); + this.deletingEmailIds.set(updatedDeletingIds); + }, + }); + } +} diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html new file mode 100644 index 000000000..a53659cc6 --- /dev/null +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html @@ -0,0 +1,25 @@ + diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.scss b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.scss new file mode 100644 index 000000000..3f1e57889 --- /dev/null +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.scss @@ -0,0 +1,5 @@ +@use "../../account-settings.component.scss" as account-settings; + +:host { + @extend .account-setting; +} diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts new file mode 100644 index 000000000..c0bd40c20 --- /dev/null +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConnectedIdentitiesComponent } from './connected-identities.component'; + +describe('ConnectedIdentitiesComponent', () => { + let component: ConnectedIdentitiesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConnectedIdentitiesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ConnectedIdentitiesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts new file mode 100644 index 000000000..e853bf35f --- /dev/null +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts @@ -0,0 +1,22 @@ +import { Store } from '@ngxs/store'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { DeleteExternalIdentity } from '@osf/features/settings/account-settings/store/account-settings.actions'; +import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; + +@Component({ + selector: 'osf-connected-identities', + imports: [], + templateUrl: './connected-identities.component.html', + styleUrl: './connected-identities.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConnectedIdentitiesComponent { + readonly #store = inject(Store); + readonly externalIdentities = this.#store.selectSignal(AccountSettingsSelectors.getExternalIdentities); + + deleteExternalIdentity(id: string): void { + this.#store.dispatch(new DeleteExternalIdentity(id)); + } +} diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.html b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.html new file mode 100644 index 000000000..662839394 --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.html @@ -0,0 +1,16 @@ +
+
+

Are you sure you want to request account deactivation?

+

This will preserve your account status

+
+ +
+ + Cancel + + + + Undo deactivation request + +
+
diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.scss b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.scss new file mode 100644 index 000000000..7d1f2ad8e --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.scss @@ -0,0 +1,7 @@ +:host { + h3, + p { + text-transform: none; + font-weight: normal; + } +} diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.spec.ts b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.spec.ts new file mode 100644 index 000000000..5c1d134eb --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CancelDeactivationComponent } from './cancel-deactivation.component'; + +describe('CancelDeactivationComponent', () => { + let component: CancelDeactivationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CancelDeactivationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CancelDeactivationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.ts b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.ts new file mode 100644 index 000000000..afd5e6917 --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.ts @@ -0,0 +1,25 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { CancelDeactivationRequest } from '@osf/features/settings/account-settings/store/account-settings.actions'; + +@Component({ + selector: 'osf-cancel-deactivation', + imports: [Button], + templateUrl: './cancel-deactivation.component.html', + styleUrl: './cancel-deactivation.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CancelDeactivationComponent { + #store = inject(Store); + dialogRef = inject(DynamicDialogRef); + + cancelDeactivation(): void { + this.#store.dispatch(CancelDeactivationRequest); + this.dialogRef.close(); + } +} diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.html b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.html new file mode 100644 index 000000000..8ec438c15 --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.html @@ -0,0 +1,16 @@ +
+
+

Are you sure you want to request account deactivation?

+

+ An OSF administrator will review your request. If accepted, you will NOT be able to reactivate your account. +

+
+ +
+ + Cancel + + + Configure +
+
diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.scss b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.scss new file mode 100644 index 000000000..7d1f2ad8e --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.scss @@ -0,0 +1,7 @@ +:host { + h3, + p { + text-transform: none; + font-weight: normal; + } +} diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.spec.ts b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.spec.ts new file mode 100644 index 000000000..15eb45a4e --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeactivationWarningComponent } from './deactivation-warning.component'; + +describe('DeactivationWarningComponent', () => { + let component: DeactivationWarningComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeactivationWarningComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DeactivationWarningComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.ts b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.ts new file mode 100644 index 000000000..3a5c07a62 --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.ts @@ -0,0 +1,25 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { DeactivateAccount } from '@osf/features/settings/account-settings/store/account-settings.actions'; + +@Component({ + selector: 'osf-deactivation-warning', + imports: [Button], + templateUrl: './deactivation-warning.component.html', + styleUrl: './deactivation-warning.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeactivationWarningComponent { + #store = inject(Store); + dialogRef = inject(DynamicDialogRef); + + deactivateAccount(): void { + this.#store.dispatch(DeactivateAccount); + this.dialogRef.close(); + } +} diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html new file mode 100644 index 000000000..fd761b712 --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html @@ -0,0 +1,30 @@ + diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss new file mode 100644 index 000000000..6863c21d0 --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss @@ -0,0 +1,12 @@ +@use "../../account-settings.component.scss" as account-settings; +@use "assets/styles/variables" as var; + +:host { + @extend .account-setting; + + h3, + p { + font-weight: 400; + text-transform: none; + } +} diff --git a/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.spec.ts b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.spec.ts similarity index 100% rename from src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.spec.ts rename to src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.spec.ts diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts new file mode 100644 index 000000000..38bdc9aa7 --- /dev/null +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts @@ -0,0 +1,48 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { Message } from 'primeng/message'; + +import { NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { CancelDeactivationComponent } from '@osf/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component'; +import { DeactivationWarningComponent } from '@osf/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component'; +import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; + +@Component({ + selector: 'osf-deactivate-account', + imports: [Button, Message, NgOptimizedImage], + templateUrl: './deactivate-account.component.html', + styleUrl: './deactivate-account.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeactivateAccountComponent { + #store = inject(Store); + dialogRef: DynamicDialogRef | null = null; + readonly #dialogService = inject(DialogService); + protected accountSettings = this.#store.selectSignal(AccountSettingsSelectors.getAccountSettings); + + deactivateAccount() { + this.dialogRef = this.#dialogService.open(DeactivationWarningComponent, { + width: '552px', + focusOnShow: false, + header: 'Deactivate account', + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + cancelDeactivation() { + this.dialogRef = this.#dialogService.open(CancelDeactivationComponent, { + width: '552px', + focusOnShow: false, + header: 'Undo deactivation request?', + closeOnEscape: true, + modal: true, + closable: true, + }); + } +} diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html new file mode 100644 index 000000000..2330dece2 --- /dev/null +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html @@ -0,0 +1,22 @@ + diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.scss b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.scss new file mode 100644 index 000000000..3f1e57889 --- /dev/null +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.scss @@ -0,0 +1,5 @@ +@use "../../account-settings.component.scss" as account-settings; + +:host { + @extend .account-setting; +} diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.spec.ts b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.spec.ts new file mode 100644 index 000000000..c4501c647 --- /dev/null +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DefaultStorageLocationComponent } from './default-storage-location.component'; + +describe('DefaultStorageLocationComponent', () => { + let component: DefaultStorageLocationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DefaultStorageLocationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DefaultStorageLocationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts new file mode 100644 index 000000000..94e6feb78 --- /dev/null +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts @@ -0,0 +1,47 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { Select } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, effect, inject, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; + +import { UserSelectors } from '@core/store/user/user.selectors'; +import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; +import { UpdateRegion } from '@osf/features/settings/account-settings/store/account-settings.actions'; +import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; +import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; + +@Component({ + selector: 'osf-default-storage-location', + imports: [Button, Select, FormsModule], + templateUrl: './default-storage-location.component.html', + styleUrl: './default-storage-location.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DefaultStorageLocationComponent { + protected readonly isMobile = toSignal(inject(IS_XSMALL)); + readonly #store = inject(Store); + + protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + protected readonly regions = this.#store.selectSignal(AccountSettingsSelectors.getRegions); + protected selectedRegion = signal(undefined); + + constructor() { + effect(() => { + const user = this.currentUser(); + const regions = this.regions(); + if (user && regions) { + const defaultRegion = regions.find((region) => region.id === user.defaultRegionId); + this.selectedRegion.set(defaultRegion); + } + }); + } + + updateLocation(): void { + if (this.selectedRegion()?.id) { + this.#store.dispatch(new UpdateRegion(this.selectedRegion()!.id)); + } + } +} diff --git a/src/app/features/settings/account-settings/components/index.ts b/src/app/features/settings/account-settings/components/index.ts new file mode 100644 index 000000000..cf85ae786 --- /dev/null +++ b/src/app/features/settings/account-settings/components/index.ts @@ -0,0 +1,9 @@ +export * from './add-email/add-email.component'; +export * from './affiliated-institutions/affiliated-institutions.component'; +export * from './change-password/change-password.component'; +export * from './connected-emails/connected-emails.component'; +export * from './connected-identities/connected-identities.component'; +export * from './deactivate-account/deactivate-account.component'; +export * from './default-storage-location/default-storage-location.component'; +export * from './share-indexing/share-indexing.component'; +export * from './two-factor-auth/two-factor-auth.component'; diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html new file mode 100644 index 000000000..658382a91 --- /dev/null +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html @@ -0,0 +1,29 @@ + diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.scss b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.scss new file mode 100644 index 000000000..3f1e57889 --- /dev/null +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.scss @@ -0,0 +1,5 @@ +@use "../../account-settings.component.scss" as account-settings; + +:host { + @extend .account-setting; +} diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.spec.ts b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.spec.ts new file mode 100644 index 000000000..e5e313f4d --- /dev/null +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ShareIndexingComponent } from './share-indexing.component'; + +describe('ShareIndexingComponent', () => { + let component: ShareIndexingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ShareIndexingComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ShareIndexingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts new file mode 100644 index 000000000..bd804e71f --- /dev/null +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts @@ -0,0 +1,44 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { RadioButton } from 'primeng/radiobutton'; + +import { ChangeDetectionStrategy, Component, effect, inject, signal } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { UserSelectors } from '@core/store/user/user.selectors'; +import { UpdateIndexing } from '@osf/features/settings/account-settings/store/account-settings.actions'; + +@Component({ + selector: 'osf-share-indexing', + imports: [Button, RadioButton, ReactiveFormsModule, FormsModule], + templateUrl: './share-indexing.component.html', + styleUrl: './share-indexing.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ShareIndexingComponent { + readonly #store = inject(Store); + protected indexing = signal(''); + protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + + updateIndexing = () => { + if (this.currentUser()?.id) { + if (this.indexing() === '1') { + this.#store.dispatch(new UpdateIndexing(true)); + } else if (this.indexing() === '0') { + this.#store.dispatch(new UpdateIndexing(false)); + } + } + }; + + constructor() { + effect(() => { + const user = this.currentUser(); + if (user?.allowIndexing) { + this.indexing.set('1'); + } else if (user?.allowIndexing === false) { + this.indexing.set('0'); + } + }); + } +} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html new file mode 100644 index 000000000..91bfe5b88 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html @@ -0,0 +1,17 @@ +
+
+

Configuring two-factor authentication will not immediately activate this feature for your account.

+

+ You will need to follow the steps that appear below to complete the activation of two-factor authentication for + your account. +

+
+ +
+ + Cancel + + + Configure +
+
diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss new file mode 100644 index 000000000..7d1f2ad8e --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss @@ -0,0 +1,7 @@ +:host { + h3, + p { + text-transform: none; + font-weight: normal; + } +} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.spec.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.spec.ts new file mode 100644 index 000000000..f6c406efb --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfigureTwoFactorComponent } from './configure-two-factor.component'; + +describe('ConfigureTwoFactorComponent', () => { + let component: ConfigureTwoFactorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfigureTwoFactorComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ConfigureTwoFactorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts new file mode 100644 index 000000000..64c8c1710 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts @@ -0,0 +1,30 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { EnableTwoFactorAuth } from '@osf/features/settings/account-settings/store/account-settings.actions'; + +@Component({ + selector: 'osf-configure-two-factor', + imports: [Button, FormsModule], + templateUrl: './configure-two-factor.component.html', + styleUrl: './configure-two-factor.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConfigureTwoFactorComponent { + #store = inject(Store); + dialogRef = inject(DynamicDialogRef); + readonly config = inject(DynamicDialogConfig); + + enableTwoFactor(): void { + const settings = this.config.data as AccountSettings; + settings.twoFactorEnabled = true; + this.#store.dispatch(EnableTwoFactorAuth); + this.dialogRef.close(); + } +} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html new file mode 100644 index 000000000..a321762b0 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html @@ -0,0 +1,10 @@ +
+

Are you sure you want to disable two-factor authentication?

+
+ + Cancel + + + Disable +
+
diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss new file mode 100644 index 000000000..7d1f2ad8e --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss @@ -0,0 +1,7 @@ +:host { + h3, + p { + text-transform: none; + font-weight: normal; + } +} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.spec.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.spec.ts new file mode 100644 index 000000000..e8557132d --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VerifyTwoFactorComponent } from './verify-two-factor.component'; + +describe('VerifyTwoFactorComponent', () => { + let component: VerifyTwoFactorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VerifyTwoFactorComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(VerifyTwoFactorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts new file mode 100644 index 000000000..694bf7ea0 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts @@ -0,0 +1,26 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { DisableTwoFactorAuth } from '@osf/features/settings/account-settings/store/account-settings.actions'; + +@Component({ + selector: 'osf-verify-two-factor', + imports: [Button], + templateUrl: './verify-two-factor.component.html', + styleUrl: './verify-two-factor.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VerifyTwoFactorComponent { + #store = inject(Store); + dialogRef = inject(DynamicDialogRef); + readonly config = inject(DynamicDialogConfig); + + disableTwoFactor() { + this.#store.dispatch(DisableTwoFactorAuth); + this.dialogRef.close(); + } +} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html new file mode 100644 index 000000000..895cdfc43 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html @@ -0,0 +1,62 @@ +@if (this.accountSettings()) { + +} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss new file mode 100644 index 000000000..3d073ac25 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss @@ -0,0 +1,16 @@ +@use "../../account-settings.component.scss" as account-settings; +@use "assets/styles/variables" as var; + +:host { + @extend .account-setting; + + h3, + p { + font-weight: 400; + text-transform: none; + } + + .token { + color: var.$red-1; + } +} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.spec.ts b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.spec.ts new file mode 100644 index 000000000..d33668379 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TwoFactorAuthComponent } from './two-factor-auth.component'; + +describe('TwoFactorAuthComponent', () => { + let component: TwoFactorAuthComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TwoFactorAuthComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TwoFactorAuthComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts new file mode 100644 index 000000000..77c679731 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts @@ -0,0 +1,92 @@ +import { Store } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { InputText } from 'primeng/inputtext'; + +import { HttpErrorResponse } from '@angular/common/http'; +import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { UserSelectors } from '@core/store/user/user.selectors'; +import { ConfigureTwoFactorComponent } from '@osf/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component'; +import { VerifyTwoFactorComponent } from '@osf/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { AccountSettingsService } from '@osf/features/settings/account-settings/services/account-settings.service'; +import { + DisableTwoFactorAuth, + SetAccountSettings, +} from '@osf/features/settings/account-settings/store/account-settings.actions'; +import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; + +import { QRCodeComponent } from 'angularx-qrcode'; + +@Component({ + selector: 'osf-two-factor-auth', + imports: [Button, QRCodeComponent, ReactiveFormsModule, InputText], + templateUrl: './two-factor-auth.component.html', + styleUrl: './two-factor-auth.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TwoFactorAuthComponent { + #store = inject(Store); + dialogRef: DynamicDialogRef | null = null; + readonly #dialogService = inject(DialogService); + readonly #accountSettingsService = inject(AccountSettingsService); + readonly accountSettings = this.#store.selectSignal(AccountSettingsSelectors.getAccountSettings); + readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + + qrCodeLink = computed(() => { + return `otpauth://totp/OSF:${this.currentUser()?.email}?secret=${this.accountSettings()?.secret}`; + }); + + verificationCode = new FormControl('', { + nonNullable: true, + validators: [Validators.required], + }); + + errorMessage = signal(''); + + // open dialog + configureTwoFactorAuth(): void { + this.dialogRef = this.#dialogService.open(ConfigureTwoFactorComponent, { + width: '520px', + focusOnShow: false, + header: 'Configure', + closeOnEscape: true, + modal: true, + closable: true, + data: this.accountSettings(), + }); + } + + openDisableDialog() { + this.dialogRef = this.#dialogService.open(VerifyTwoFactorComponent, { + width: '520px', + focusOnShow: false, + header: 'Disable', + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + enableTwoFactor(): void { + this.#accountSettingsService.updateSettings({ two_factor_verification: this.verificationCode.value }).subscribe({ + next: (response: AccountSettings) => { + this.#store.dispatch(new SetAccountSettings(response)); + }, + error: (error: HttpErrorResponse) => { + if (error.error?.errors?.[0]?.detail) { + this.errorMessage.set(error.error.errors[0].detail); + } else { + this.errorMessage.set('Verification code is invalid. Please try again.'); + } + }, + }); + } + + disableTwoFactor(): void { + this.#store.dispatch(DisableTwoFactorAuth); + } +} diff --git a/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.html b/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.html deleted file mode 100644 index eb014acc5..000000000 --- a/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.html +++ /dev/null @@ -1,9 +0,0 @@ -
-

Are you sure you want to to deactivate your account?

- -
- - - Deactivate -
-
diff --git a/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.scss b/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.ts b/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.ts deleted file mode 100644 index 84880cbc1..000000000 --- a/src/app/features/settings/account-settings/deactivate-account/deactivate-account/deactivate-account.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Button } from 'primeng/button'; - -import { ChangeDetectionStrategy, Component } from '@angular/core'; - -@Component({ - selector: 'osf-deactivate-account', - imports: [Button], - templateUrl: './deactivate-account.component.html', - styleUrl: './deactivate-account.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DeactivateAccountComponent {} diff --git a/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts b/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts new file mode 100644 index 000000000..f8751d4e0 --- /dev/null +++ b/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts @@ -0,0 +1,15 @@ +import { ApiData } from '@core/services/json-api/json-api.entity'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { AccountSettingsResponse } from '@osf/features/settings/account-settings/models/responses/get-account-settings-response.entity'; + +export function MapAccountSettings(data: ApiData): AccountSettings { + return { + twoFactorEnabled: data.attributes.two_factor_enabled, + twoFactorConfirmed: data.attributes.two_factor_confirmed, + subscribeOsfGeneralEmail: data.attributes.subscribe_osf_general_email, + subscribeOsfHelpEmail: data.attributes.subscribe_osf_help_email, + deactivationRequested: data.attributes.deactivation_requested, + contactedDeactivation: data.attributes.contacted_deactivation, + secret: data.attributes.secret, + }; +} diff --git a/src/app/features/settings/account-settings/mappers/emails.mapper.ts b/src/app/features/settings/account-settings/mappers/emails.mapper.ts new file mode 100644 index 000000000..c44562cd1 --- /dev/null +++ b/src/app/features/settings/account-settings/mappers/emails.mapper.ts @@ -0,0 +1,22 @@ +import { ApiData } from '@core/services/json-api/json-api.entity'; +import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-entities/account-email.entity'; +import { AccountEmailResponse } from '@osf/features/settings/account-settings/models/responses/list-emails.entity'; + +export function MapEmails(emails: ApiData[]): AccountEmail[] { + const accountEmails: AccountEmail[] = []; + emails.forEach((email) => { + accountEmails.push(MapEmail(email)); + }); + return accountEmails; +} + +export function MapEmail(email: ApiData): AccountEmail { + return { + id: email.id, + emailAddress: email.attributes.email_address, + confirmed: email.attributes.confirmed, + verified: email.attributes.verified, + primary: email.attributes.primary, + isMerge: email.attributes.is_merge, + }; +} diff --git a/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts b/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts new file mode 100644 index 000000000..924207de1 --- /dev/null +++ b/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts @@ -0,0 +1,15 @@ +import { ApiData } from '@core/services/json-api/json-api.entity'; +import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-entities/external-institution.entity'; +import { ExternalIdentityResponse } from '@osf/features/settings/account-settings/models/responses/list-identities-response.entity'; + +export function MapExternalIdentities(data: ApiData[]): ExternalIdentity[] { + const identities: ExternalIdentity[] = []; + for (const item of data) { + identities.push({ + id: item.id, + externalId: item.attributes.external_id, + status: item.attributes.status, + }); + } + return identities; +} diff --git a/src/app/features/settings/account-settings/mappers/regions.mapper.ts b/src/app/features/settings/account-settings/mappers/regions.mapper.ts new file mode 100644 index 000000000..ac790567d --- /dev/null +++ b/src/app/features/settings/account-settings/mappers/regions.mapper.ts @@ -0,0 +1,18 @@ +import { ApiData } from '@core/services/json-api/json-api.entity'; +import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; + +export function MapRegions(data: ApiData<{ name: string }, null, null>[]): Region[] { + const regions: Region[] = []; + for (const region of data) { + regions.push(MapRegion(region)); + } + + return regions; +} + +export function MapRegion(data: ApiData<{ name: string }, null, null>): Region { + return { + id: data.id, + name: data.attributes.name, + }; +} diff --git a/src/app/features/settings/account-settings/models/osf-entities/account-email.entity.ts b/src/app/features/settings/account-settings/models/osf-entities/account-email.entity.ts new file mode 100644 index 000000000..982990912 --- /dev/null +++ b/src/app/features/settings/account-settings/models/osf-entities/account-email.entity.ts @@ -0,0 +1,8 @@ +export interface AccountEmail { + id: string; + emailAddress: string; + confirmed: boolean; + verified: boolean; + primary: boolean; + isMerge: boolean; +} diff --git a/src/app/features/settings/account-settings/models/osf-entities/account-settings.entity.ts b/src/app/features/settings/account-settings/models/osf-entities/account-settings.entity.ts new file mode 100644 index 000000000..bd6a27ae3 --- /dev/null +++ b/src/app/features/settings/account-settings/models/osf-entities/account-settings.entity.ts @@ -0,0 +1,9 @@ +export interface AccountSettings { + twoFactorEnabled: boolean; + twoFactorConfirmed: boolean; + subscribeOsfGeneralEmail: boolean; + subscribeOsfHelpEmail: boolean; + deactivationRequested: boolean; + contactedDeactivation: boolean; + secret: string; +} diff --git a/src/app/features/settings/account-settings/models/osf-entities/external-institution.entity.ts b/src/app/features/settings/account-settings/models/osf-entities/external-institution.entity.ts new file mode 100644 index 000000000..1f76db107 --- /dev/null +++ b/src/app/features/settings/account-settings/models/osf-entities/external-institution.entity.ts @@ -0,0 +1,5 @@ +export interface ExternalIdentity { + id: string; + externalId: string; + status: string; +} diff --git a/src/app/features/settings/account-settings/models/osf-entities/region.entity.ts b/src/app/features/settings/account-settings/models/osf-entities/region.entity.ts new file mode 100644 index 000000000..4068f4f1e --- /dev/null +++ b/src/app/features/settings/account-settings/models/osf-entities/region.entity.ts @@ -0,0 +1,4 @@ +export interface Region { + id: string; + name: string; +} diff --git a/src/app/features/settings/account-settings/models/responses/get-account-settings-response.entity.ts b/src/app/features/settings/account-settings/models/responses/get-account-settings-response.entity.ts new file mode 100644 index 000000000..5cab32961 --- /dev/null +++ b/src/app/features/settings/account-settings/models/responses/get-account-settings-response.entity.ts @@ -0,0 +1,13 @@ +import { ApiData } from '@core/services/json-api/json-api.entity'; + +export type GetAccountSettingsResponse = ApiData; + +export interface AccountSettingsResponse { + two_factor_enabled: boolean; + two_factor_confirmed: boolean; + subscribe_osf_general_email: boolean; + subscribe_osf_help_email: boolean; + deactivation_requested: boolean; + contacted_deactivation: boolean; + secret: string; +} diff --git a/src/app/features/settings/account-settings/models/responses/get-email-response.entity.ts b/src/app/features/settings/account-settings/models/responses/get-email-response.entity.ts new file mode 100644 index 000000000..9a2a957a9 --- /dev/null +++ b/src/app/features/settings/account-settings/models/responses/get-email-response.entity.ts @@ -0,0 +1,4 @@ +import { ApiData, JsonApiResponse } from '@core/services/json-api/json-api.entity'; +import { AccountEmailResponse } from '@osf/features/settings/account-settings/models/responses/list-emails.entity'; + +export type GetEmailResponse = JsonApiResponse, null>; diff --git a/src/app/features/settings/account-settings/models/responses/get-regions-response.entity.ts b/src/app/features/settings/account-settings/models/responses/get-regions-response.entity.ts new file mode 100644 index 000000000..099e98a95 --- /dev/null +++ b/src/app/features/settings/account-settings/models/responses/get-regions-response.entity.ts @@ -0,0 +1,4 @@ +import { ApiData, JsonApiResponse } from '@core/services/json-api/json-api.entity'; + +export type GetRegionsResponse = JsonApiResponse[], null>; +export type GetRegionResponse = JsonApiResponse, null>; diff --git a/src/app/features/settings/account-settings/models/responses/list-emails.entity.ts b/src/app/features/settings/account-settings/models/responses/list-emails.entity.ts new file mode 100644 index 000000000..0a74ab0c4 --- /dev/null +++ b/src/app/features/settings/account-settings/models/responses/list-emails.entity.ts @@ -0,0 +1,11 @@ +import { ApiData, JsonApiResponse } from '@core/services/json-api/json-api.entity'; + +export type ListEmailsResponse = JsonApiResponse[], null>; + +export interface AccountEmailResponse { + email_address: string; + confirmed: boolean; + verified: boolean; + primary: boolean; + is_merge: boolean; +} diff --git a/src/app/features/settings/account-settings/models/responses/list-identities-response.entity.ts b/src/app/features/settings/account-settings/models/responses/list-identities-response.entity.ts new file mode 100644 index 000000000..f2c2af49a --- /dev/null +++ b/src/app/features/settings/account-settings/models/responses/list-identities-response.entity.ts @@ -0,0 +1,8 @@ +import { ApiData, JsonApiResponse } from '@core/services/json-api/json-api.entity'; + +export type ListIdentitiesResponse = JsonApiResponse[], null>; + +export interface ExternalIdentityResponse { + external_id: string; + status: string; +} diff --git a/src/app/features/settings/account-settings/services/account-settings.service.ts b/src/app/features/settings/account-settings/services/account-settings.service.ts new file mode 100644 index 000000000..f3c7b86d5 --- /dev/null +++ b/src/app/features/settings/account-settings/services/account-settings.service.ts @@ -0,0 +1,232 @@ +import { Store } from '@ngxs/store'; + +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { UserUS } from '@core/services/json-api/underscore-entites/user/user-us.entity'; +import { mapUserUStoUser } from '@core/services/mappers/users/users.mapper'; +import { User } from '@core/services/user/user.entity'; +import { UserSelectors } from '@core/store/user/user.selectors'; +import { ApiData, JsonApiResponse } from '@osf/core/services/json-api/json-api.entity'; +import { MapAccountSettings } from '@osf/features/settings/account-settings/mappers/account-settings.mapper'; +import { MapEmail, MapEmails } from '@osf/features/settings/account-settings/mappers/emails.mapper'; +import { MapExternalIdentities } from '@osf/features/settings/account-settings/mappers/external-identities.mapper'; +import { MapRegions } from '@osf/features/settings/account-settings/mappers/regions.mapper'; +import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-entities/account-email.entity'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-entities/external-institution.entity'; +import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; +import { GetAccountSettingsResponse } from '@osf/features/settings/account-settings/models/responses/get-account-settings-response.entity'; +import { GetEmailResponse } from '@osf/features/settings/account-settings/models/responses/get-email-response.entity'; +import { GetRegionsResponse } from '@osf/features/settings/account-settings/models/responses/get-regions-response.entity'; +import { + AccountEmailResponse, + ListEmailsResponse, +} from '@osf/features/settings/account-settings/models/responses/list-emails.entity'; +import { ListIdentitiesResponse } from '@osf/features/settings/account-settings/models/responses/list-identities-response.entity'; + +import { environment } from '../../../../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class AccountSettingsService { + #store = inject(Store); + #jsonApiService = inject(JsonApiService); + #currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + + getEmails(): Observable { + const params: Record = { + page: '1', + 'page[size]': '10', + }; + + return this.#jsonApiService + .get(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/emails`, params) + .pipe(map((response) => MapEmails(response.data))); + } + + getEmail( + emailId: string, + userId: string, + params: Record | undefined = undefined + ): Observable { + return this.#jsonApiService + .get(`${environment.apiUrl}/users/${userId}/settings/emails/${emailId}`, params) + .pipe(map((response) => MapEmail(response.data))); + } + + resendConfirmation(emailId: string, userId: string): Observable { + const params: Record = { + resend_confirmation: 'true', + }; + + return this.getEmail(emailId, userId, params); + } + + addEmail(email: string): Observable { + const body = { + data: { + attributes: { + email_address: email, + }, + relationships: { + user: { + data: { + id: this.#currentUser()?.id, + type: 'users', + }, + }, + }, + type: 'user_emails', + }, + }; + + return this.#jsonApiService + .post< + ApiData + >(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/emails/`, body) + .pipe(map((response) => MapEmail(response))); + } + + deleteEmail(emailId: string): Observable { + return this.#jsonApiService.delete( + `${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/emails/${emailId}` + ); + } + + verifyEmail(userId: string, emailId: string): Observable { + const body = { + data: { + id: emailId, + attributes: { + verified: true, + }, + type: 'user_emails', + }, + }; + + return this.#jsonApiService + .patch< + ApiData + >(`${environment.apiUrl}/users/${userId}/settings/emails/${emailId}/`, body) + .pipe(map((response) => MapEmail(response))); + } + + makePrimary(emailId: string): Observable { + const body = { + data: { + id: emailId, + attributes: { + primary: true, + }, + type: 'user_emails', + }, + }; + + return this.#jsonApiService + .patch< + ApiData + >(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/emails/${emailId}/`, body) + .pipe(map((response) => MapEmail(response))); + } + + getRegions(): Observable { + return this.#jsonApiService + .get(`${environment.apiUrl}/regions/`) + .pipe(map((response) => MapRegions(response.data))); + } + + updateLocation(locationId: string): Observable { + const body = { + data: { + id: this.#currentUser()?.id, + attributes: {}, + relationships: { + default_region: { + data: { + type: 'regions', + id: locationId, + }, + }, + }, + type: 'users', + }, + }; + + return this.#jsonApiService + .patch(`${environment.apiUrl}/users/${this.#currentUser()?.id}`, body) + .pipe(map((user) => mapUserUStoUser(user))); + } + + updateIndexing(allowIndexing: boolean): Observable { + const body = { + data: { + id: this.#currentUser()?.id, + attributes: { + allow_indexing: allowIndexing, + }, + relationships: {}, + type: 'users', + }, + }; + + return this.#jsonApiService + .patch(`${environment.apiUrl}/users/${this.#currentUser()?.id}`, body) + .pipe(map((user) => mapUserUStoUser(user))); + } + + updatePassword(oldPassword: string, newPassword: string): Observable { + const body = { + data: { + type: 'user_passwords', + attributes: { + existing_password: oldPassword, + new_password: newPassword, + }, + }, + }; + + return this.#jsonApiService.post(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/password`, body); + } + + getExternalIdentities(): Observable { + const params: Record = { + page: '1', + 'page[size]': '10', + }; + + return this.#jsonApiService + .get(`${environment.apiUrl}/users/me/settings/identities/`, params) + .pipe(map((response) => MapExternalIdentities(response.data))); + } + + deleteExternalIdentity(id: string): Observable { + return this.#jsonApiService.delete(`${environment.apiUrl}/users/me/settings/identities/${id}`); + } + + getSettings(): Observable { + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings`) + .pipe(map((response) => MapAccountSettings(response.data))); + } + + updateSettings(settings: Record): Observable { + const body = { + data: { + id: this.#currentUser()?.id, + attributes: settings, + relationships: {}, + type: 'user_settings', + }, + }; + + return this.#jsonApiService + .patch(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings`, body) + .pipe(map((response) => MapAccountSettings(response))); + } +} diff --git a/src/app/features/settings/account-settings/store/account-settings.actions.ts b/src/app/features/settings/account-settings/store/account-settings.actions.ts new file mode 100644 index 000000000..1d6676fd5 --- /dev/null +++ b/src/app/features/settings/account-settings/store/account-settings.actions.ts @@ -0,0 +1,109 @@ +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; + +export class GetEmails { + static readonly type = '[AccountSettings] Get Emails'; +} + +export class AddEmail { + static readonly type = '[AccountSettings] Add Email'; + + constructor(public email: string) {} +} + +export class DeleteEmail { + static readonly type = '[AccountSettings] Remove Email'; + + constructor(public email: string) {} +} + +export class VerifyEmail { + static readonly type = '[AccountSettings] Verify Email'; + + constructor( + public userId: string, + public emailId: string + ) {} +} + +export class MakePrimary { + static readonly type = '[AccountSettings] Make Primary'; + + constructor(public userId: string) {} +} + +export class GetRegions { + static readonly type = '[AccountSettings] Get Regions'; +} + +export class UpdateRegion { + static readonly type = '[AccountSettings] Update Region'; + + constructor(public regionId: string) {} +} + +export class UpdateIndexing { + static readonly type = '[AccountSettings] Update Indexing'; + + constructor(public allowIndexing: boolean) {} +} + +export class GetExternalIdentities { + static readonly type = '[AccountSettings] Get External Identities'; +} + +export class DeleteExternalIdentity { + static readonly type = '[AccountSettings] Delete ExternalIdentities'; + + constructor(public externalId: string) {} +} + +export class GetUserInstitutions { + static readonly type = '[AccountSettings] Get User Institutions'; +} + +export class DeleteUserInstitution { + static readonly type = '[AccountSettings] Delete User Institution'; + + constructor( + public id: string, + public userId: string + ) {} +} + +export class GetAccountSettings { + static readonly type = '[AccountSettings] Get AccountSettings'; +} + +export class UpdateAccountSettings { + static readonly type = '[AccountSettings] Update Account Settings'; + + constructor(public accountSettings: Record) {} +} + +export class DisableTwoFactorAuth { + static readonly type = '[AccountSettings] Disable Two-Factor Auth'; +} + +export class EnableTwoFactorAuth { + static readonly type = '[AccountSettings] Enable Two-Factor Auth'; +} + +export class VerifyTwoFactorAuth { + static readonly type = '[AccountSettings] Verify Two-Factor Auth'; + + constructor(public code: string) {} +} + +export class SetAccountSettings { + static readonly type = '[AccountSettings] SetAccountSettings'; + + constructor(public accountSettings: AccountSettings) {} +} + +export class DeactivateAccount { + static readonly type = '[AccountSettings] Deactivate Account'; +} + +export class CancelDeactivationRequest { + static readonly type = '[AccountSettings] Cancel Deactivation Request'; +} diff --git a/src/app/features/settings/account-settings/store/account-settings.model.ts b/src/app/features/settings/account-settings/store/account-settings.model.ts new file mode 100644 index 000000000..1243cce9c --- /dev/null +++ b/src/app/features/settings/account-settings/store/account-settings.model.ts @@ -0,0 +1,14 @@ +import { Institution } from '@osf/features/institutions/entities/institutions.models'; +import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-entities/account-email.entity'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-entities/external-institution.entity'; +import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; + +export interface AccountSettingsStateModel { + emails: AccountEmail[]; + emailsLoading: boolean; + regions: Region[]; + externalIdentities: ExternalIdentity[]; + accountSettings: AccountSettings; + userInstitutions: Institution[]; +} diff --git a/src/app/features/settings/account-settings/store/account-settings.selectors.ts b/src/app/features/settings/account-settings/store/account-settings.selectors.ts new file mode 100644 index 000000000..f39380270 --- /dev/null +++ b/src/app/features/settings/account-settings/store/account-settings.selectors.ts @@ -0,0 +1,51 @@ +import { Selector } from '@ngxs/store'; + +import { Institution } from '@osf/features/institutions/entities/institutions.models'; +import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-entities/account-email.entity'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-entities/external-institution.entity'; +import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; +import { AccountSettingsStateModel } from '@osf/features/settings/account-settings/store/account-settings.model'; +import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; + +export class AccountSettingsSelectors { + @Selector([AccountSettingsState]) + static getEmails(state: AccountSettingsStateModel): AccountEmail[] { + return state.emails; + } + + @Selector([AccountSettingsState]) + static isEmailsLoading(state: AccountSettingsStateModel): boolean { + return state.emailsLoading; + } + + @Selector([AccountSettingsState]) + static getRegions(state: AccountSettingsStateModel): Region[] { + return state.regions; + } + + @Selector([AccountSettingsState]) + static getExternalIdentities(state: AccountSettingsStateModel): ExternalIdentity[] { + return state.externalIdentities; + } + + @Selector([AccountSettingsState]) + static getAccountSettings(state: AccountSettingsStateModel): AccountSettings | undefined { + return state.accountSettings; + } + + @Selector([AccountSettingsState]) + static getTwoFactorEnabled(state: AccountSettingsStateModel): boolean { + return state.accountSettings?.twoFactorEnabled ?? false; + } + + @Selector([AccountSettingsState]) + static getTwoFactorSecret(state: AccountSettingsStateModel): string { + return state.accountSettings?.secret ?? ''; + } + + @Selector([AccountSettingsState]) + static getUserInstitutions(state: AccountSettingsStateModel): Institution[] { + return state.userInstitutions; + } +} diff --git a/src/app/features/settings/account-settings/store/account-settings.state.ts b/src/app/features/settings/account-settings/store/account-settings.state.ts new file mode 100644 index 000000000..a62301fd1 --- /dev/null +++ b/src/app/features/settings/account-settings/store/account-settings.state.ts @@ -0,0 +1,271 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { finalize, tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { SetCurrentUser } from '@core/store/user'; +import { InstitutionsService } from '@osf/features/institutions/institutions.service'; +import { AccountSettingsService } from '@osf/features/settings/account-settings/services/account-settings.service'; +import { + AddEmail, + CancelDeactivationRequest, + DeactivateAccount, + DeleteEmail, + DeleteExternalIdentity, + DeleteUserInstitution, + DisableTwoFactorAuth, + EnableTwoFactorAuth, + GetAccountSettings, + GetEmails, + GetExternalIdentities, + GetRegions, + GetUserInstitutions, + MakePrimary, + SetAccountSettings, + UpdateAccountSettings, + UpdateIndexing, + UpdateRegion, + VerifyEmail, +} from '@osf/features/settings/account-settings/store/account-settings.actions'; +import { AccountSettingsStateModel } from '@osf/features/settings/account-settings/store/account-settings.model'; + +@Injectable() +@State({ + name: 'accountSettings', + defaults: { + emails: [], + emailsLoading: false, + regions: [], + externalIdentities: [], + accountSettings: { + twoFactorEnabled: false, + twoFactorConfirmed: false, + subscribeOsfGeneralEmail: false, + subscribeOsfHelpEmail: false, + deactivationRequested: false, + contactedDeactivation: false, + secret: '', + }, + userInstitutions: [], + }, +}) +export class AccountSettingsState { + #accountSettingsService = inject(AccountSettingsService); + #institutionsService = inject(InstitutionsService); + + @Action(GetEmails) + getEmails(ctx: StateContext) { + ctx.patchState({ + emailsLoading: true, + }); + return this.#accountSettingsService.getEmails().pipe( + tap({ + next: (emails) => { + ctx.patchState({ + emails: emails, + }); + }, + }), + finalize(() => ctx.patchState({ emailsLoading: false })) + ); + } + + @Action(AddEmail) + addEmail(ctx: StateContext, action: AddEmail) { + return this.#accountSettingsService.addEmail(action.email).pipe( + tap((email) => { + if (email.emailAddress && !email.confirmed) { + ctx.patchState({ + emails: [email, ...ctx.getState().emails], + }); + } + }) + ); + } + + @Action(DeleteEmail) + deleteEmail(ctx: StateContext, action: DeleteEmail) { + return this.#accountSettingsService.deleteEmail(action.email).pipe( + tap({ + next: () => { + const state = ctx.getState(); + ctx.setState({ + ...state, + emails: state.emails.filter((email) => email.id !== action.email), + }); + }, + }) + ); + } + + @Action(VerifyEmail) + verifyEmail(ctx: StateContext, action: VerifyEmail) { + return this.#accountSettingsService.verifyEmail(action.userId, action.emailId).pipe( + tap((email) => { + if (email.verified) { + ctx.dispatch(new GetEmails()); + } + }) + ); + } + + @Action(MakePrimary) + makePrimary(ctx: StateContext, action: MakePrimary) { + return this.#accountSettingsService.makePrimary(action.userId).pipe( + tap((email) => { + if (email.verified) { + ctx.dispatch(new GetEmails()); + } + }) + ); + } + + @Action(GetRegions) + getRegions(ctx: StateContext) { + return this.#accountSettingsService.getRegions().pipe( + tap({ + next: (regions) => ctx.patchState({ regions: regions }), + }) + ); + } + + @Action(UpdateRegion) + updateRegion(ctx: StateContext, action: UpdateRegion) { + return this.#accountSettingsService.updateLocation(action.regionId).pipe( + tap({ + next: (user) => { + ctx.dispatch(new SetCurrentUser(user)); + }, + }) + ); + } + + @Action(UpdateIndexing) + updateIndexing(ctx: StateContext, action: UpdateIndexing) { + return this.#accountSettingsService.updateIndexing(action.allowIndexing).pipe( + tap({ + next: (user) => { + ctx.dispatch(new SetCurrentUser(user)); + }, + }) + ); + } + + @Action(GetExternalIdentities) + getExternalIdentities(ctx: StateContext) { + return this.#accountSettingsService.getExternalIdentities().pipe( + tap({ + next: (identities) => ctx.patchState({ externalIdentities: identities }), + }) + ); + } + + @Action(DeleteExternalIdentity) + deleteExternalIdentity(ctx: StateContext, action: DeleteExternalIdentity) { + return this.#accountSettingsService.deleteExternalIdentity(action.externalId).pipe( + tap(() => { + ctx.dispatch(GetExternalIdentities); + }) + ); + } + + @Action(GetUserInstitutions) + getUserInstitutions(ctx: StateContext) { + return this.#institutionsService + .getUserInstitutions() + .pipe(tap((userInstitutions) => ctx.patchState({ userInstitutions }))); + } + + @Action(DeleteUserInstitution) + deleteUserInstitution(ctx: StateContext, action: DeleteUserInstitution) { + return this.#institutionsService.deleteUserInstitution(action.id, action.userId).pipe( + tap(() => { + ctx.dispatch(GetUserInstitutions); + }) + ); + } + + @Action(GetAccountSettings) + getAccountSettings(ctx: StateContext) { + return this.#accountSettingsService.getSettings().pipe( + tap({ + next: (settings) => { + ctx.patchState({ + accountSettings: settings, + }); + }, + }) + ); + } + + @Action(UpdateAccountSettings) + updateAccountSettings(ctx: StateContext, action: UpdateAccountSettings) { + return this.#accountSettingsService.updateSettings(action.accountSettings).pipe( + tap({ + next: (settings) => { + ctx.patchState({ + accountSettings: settings, + }); + }, + }) + ); + } + + @Action(DisableTwoFactorAuth) + disableTwoFactorAuth(ctx: StateContext) { + return this.#accountSettingsService.updateSettings({ two_factor_enabled: 'false' }).pipe( + tap({ + next: (settings) => { + ctx.patchState({ + accountSettings: settings, + }); + }, + }) + ); + } + + @Action(EnableTwoFactorAuth) + enableTwoFactorAuth(ctx: StateContext) { + return this.#accountSettingsService.updateSettings({ two_factor_enabled: 'true' }).pipe( + tap({ + next: (settings) => { + ctx.patchState({ + accountSettings: settings, + }); + }, + }) + ); + } + + @Action(SetAccountSettings) + setAccountSettings(ctx: StateContext, action: SetAccountSettings) { + ctx.patchState({ accountSettings: action.accountSettings }); + } + + @Action(DeactivateAccount) + deactivateAccount(ctx: StateContext) { + return this.#accountSettingsService.updateSettings({ deactivation_requested: 'true' }).pipe( + tap({ + next: (settings) => { + ctx.patchState({ + accountSettings: settings, + }); + }, + }) + ); + } + + @Action(CancelDeactivationRequest) + cancelDeactivationRequest(ctx: StateContext) { + return this.#accountSettingsService.updateSettings({ deactivation_requested: 'false' }).pipe( + tap({ + next: (settings) => { + ctx.patchState({ + accountSettings: settings, + }); + }, + }) + ); + } +} diff --git a/src/app/features/settings/index.ts b/src/app/features/settings/index.ts index 3cd1783d9..3c33a3305 100644 --- a/src/app/features/settings/index.ts +++ b/src/app/features/settings/index.ts @@ -1,3 +1,2 @@ export * from './account-settings/account-settings.component'; -export * from './account-settings/account-settings.route'; export * from './profile-settings/profile-settings.component'; diff --git a/src/assets/icons/source/warning-sign.svg b/src/assets/icons/source/warning-sign.svg index 2da513414..96c555c55 100644 --- a/src/assets/icons/source/warning-sign.svg +++ b/src/assets/icons/source/warning-sign.svg @@ -1,5 +1,3 @@ - - - + + - diff --git a/src/assets/styles/overrides/message.scss b/src/assets/styles/overrides/message.scss index cdaad5d21..a54bf305e 100644 --- a/src/assets/styles/overrides/message.scss +++ b/src/assets/styles/overrides/message.scss @@ -17,3 +17,17 @@ .p-message-error { background-color: var.$red-1; } + +.warning-message { + .p-message { + width: fit-content; + height: fit-content; + color: var.$red-1; + background: var.$red-2; + padding: 1.7rem; + + .p-message-text { + font-weight: 700; + } + } +} diff --git a/src/assets/styles/overrides/password.scss b/src/assets/styles/overrides/password.scss new file mode 100644 index 000000000..f4047de5f --- /dev/null +++ b/src/assets/styles/overrides/password.scss @@ -0,0 +1,7 @@ +.no-meter { + .p-password { + .p-password-overlay { + display: none; + } + } +} diff --git a/src/assets/styles/overrides/spinner.scss b/src/assets/styles/overrides/spinner.scss new file mode 100644 index 000000000..792ba84e3 --- /dev/null +++ b/src/assets/styles/overrides/spinner.scss @@ -0,0 +1,5 @@ +@use "assets/styles/variables" as var; + +.p-progressspinner-circle { + stroke: var.$pr-blue-1 !important; +} diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss index 7120cc0b7..01460d575 100644 --- a/src/assets/styles/styles.scss +++ b/src/assets/styles/styles.scss @@ -28,6 +28,9 @@ @use "./overrides/tag"; @use "./overrides/dataview"; @use "./overrides/menu"; +@use "./overrides/spinner"; +@use "./overrides/password"; + @layer base, primeng, reset; @layer base { From 9bb52238a7a6769b45b705bd027136e368f34567 Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Tue, 13 May 2025 20:07:09 +0300 Subject: [PATCH 2/4] chore(account-settings): add translates, minor fixes --- .../breadcrumb/breadcrumb.component.html | 2 +- .../confirm-email.component.html | 2 +- .../confirm-email.component.scss | 3 +- .../home/components/logged-out/data.ts | 6 +- .../logged-out/home-logged-out.component.html | 98 +++-------- .../logged-out/home-logged-out.component.scss | 2 +- .../account-settings.component.scss | 4 + .../add-email/add-email.component.html | 30 +++- .../add-email/add-email.component.ts | 4 +- .../affiliated-institutions.component.html | 10 +- .../affiliated-institutions.component.ts | 6 +- .../change-password.component.html | 41 +++-- .../change-password.component.scss | 2 +- .../change-password.component.ts | 12 +- .../connected-emails.component.html | 29 ++-- .../connected-emails.component.ts | 7 +- .../connected-identities.component.html | 6 +- .../connected-identities.component.ts | 4 +- .../cancel-deactivation.component.html | 29 +++- .../cancel-deactivation.component.scss | 8 +- .../cancel-deactivation.component.ts | 4 +- .../deactivation-warning.component.html | 27 ++- .../deactivation-warning.component.scss | 8 +- .../deactivation-warning.component.ts | 4 +- .../deactivate-account.component.html | 19 ++- .../deactivate-account.component.scss | 7 - .../deactivate-account.component.ts | 9 +- .../default-storage-location.component.html | 8 +- .../default-storage-location.component.ts | 6 +- .../enums/share-indexing.enum.ts | 5 + .../share-indexing.component.html | 19 +-- .../share-indexing.component.ts | 15 +- .../configure-two-factor.component.html | 27 ++- .../configure-two-factor.component.scss | 8 +- .../configure-two-factor.component.ts | 6 +- .../two-factor-auth/components/index.ts | 2 + .../verify-two-factor.component.html | 23 ++- .../verify-two-factor.component.scss | 8 +- .../verify-two-factor.component.ts | 4 +- .../two-factor-auth.component.html | 60 ++++--- .../two-factor-auth.component.scss | 9 +- .../two-factor-auth.component.ts | 25 ++- .../mappers/account-settings.mapper.ts | 2 +- .../account-settings/mappers/emails.mapper.ts | 2 +- .../mappers/external-identities.mapper.ts | 2 +- .../account-settings/mappers/index.ts | 4 + .../mappers/regions.mapper.ts | 2 +- .../settings/account-settings/models/index.ts | 9 + .../account-email.model.ts} | 0 .../account-settings.model.ts} | 0 .../external-institution.model.ts} | 0 .../region.model.ts} | 0 .../services/account-settings.service.ts | 27 ++- .../account-settings/services/index.ts | 1 + .../store/account-settings.actions.ts | 2 +- .../store/account-settings.model.ts | 8 +- .../store/account-settings.selectors.ts | 8 +- .../store/account-settings.state.ts | 3 +- src/assets/i18n/en.json | 154 +++++++++++++++++- src/assets/icons/source/arrow-down.svg | 6 +- 60 files changed, 545 insertions(+), 293 deletions(-) create mode 100644 src/app/features/settings/account-settings/components/share-indexing/enums/share-indexing.enum.ts create mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/index.ts create mode 100644 src/app/features/settings/account-settings/mappers/index.ts create mode 100644 src/app/features/settings/account-settings/models/index.ts rename src/app/features/settings/account-settings/models/{osf-entities/account-email.entity.ts => osf-models/account-email.model.ts} (100%) rename src/app/features/settings/account-settings/models/{osf-entities/account-settings.entity.ts => osf-models/account-settings.model.ts} (100%) rename src/app/features/settings/account-settings/models/{osf-entities/external-institution.entity.ts => osf-models/external-institution.model.ts} (100%) rename src/app/features/settings/account-settings/models/{osf-entities/region.entity.ts => osf-models/region.model.ts} (100%) create mode 100644 src/app/features/settings/account-settings/services/index.ts diff --git a/src/app/core/components/breadcrumb/breadcrumb.component.html b/src/app/core/components/breadcrumb/breadcrumb.component.html index 291606465..52e6b0d74 100644 --- a/src/app/core/components/breadcrumb/breadcrumb.component.html +++ b/src/app/core/components/breadcrumb/breadcrumb.component.html @@ -1,4 +1,4 @@ -@if (parsedUrl()[0] !== 'home') { +@if (!parsedUrl()[0].includes('home')) {
- better-research + better-research
@@ -45,8 +40,8 @@

- search icon - search icon + search icon + search icon

@@ -60,8 +55,8 @@

- search icon - search icon + search icon + search icon

@@ -75,8 +70,8 @@

- search icon - search icon + search icon + search icon

@@ -90,7 +85,7 @@

- search icon + search icon

@@ -171,53 +166,28 @@

- incommon - sso + incommon + sso
google-scholar - orcid + orcid
- mendeley - zotero + mendeley + zotero
- dropbox - google-drive + dropbox + google-drive

@@ -227,13 +197,8 @@

{{ 'home.loggedOut.integrations.categories.authentication' | translate }}

- incommon - sso + incommon + sso
@@ -241,43 +206,28 @@

{{ 'home.loggedOut.integrations.categories.discovery' | translate }}

google-scholar - orcid + orcid

{{ 'home.loggedOut.integrations.categories.references' | translate }}

- mendeley - zotero + mendeley + zotero

{{ 'home.loggedOut.integrations.categories.storage' | translate }}

- dropbox - google-drive + dropbox + google-drive

} @else { diff --git a/src/app/features/home/components/logged-out/home-logged-out.component.scss b/src/app/features/home/components/logged-out/home-logged-out.component.scss index 4d0c0f215..c2c329b42 100644 --- a/src/app/features/home/components/logged-out/home-logged-out.component.scss +++ b/src/app/features/home/components/logged-out/home-logged-out.component.scss @@ -1,4 +1,4 @@ -@use "../../../../../assets/styles/variables" as var; +@use "assets/styles/variables" as var; :host { .home-container { diff --git a/src/app/features/settings/account-settings/account-settings.component.scss b/src/app/features/settings/account-settings/account-settings.component.scss index 9ec01d6c2..1164df561 100644 --- a/src/app/features/settings/account-settings/account-settings.component.scss +++ b/src/app/features/settings/account-settings/account-settings.component.scss @@ -42,6 +42,8 @@ flex-direction: column; gap: 1.7rem; border-radius: 0.5rem; + font-weight: 400; + text-transform: none; &-link { font-weight: 600; @@ -65,6 +67,8 @@ &-description { line-height: 2rem; + text-transform: none; + font-weight: 400; } &-action { diff --git a/src/app/features/settings/account-settings/components/add-email/add-email.component.html b/src/app/features/settings/account-settings/components/add-email/add-email.component.html index a8941bc0c..66059257f 100644 --- a/src/app/features/settings/account-settings/components/add-email/add-email.component.html +++ b/src/app/features/settings/account-settings/components/add-email/add-email.component.html @@ -1,16 +1,34 @@
- - + +
- - Cancel + - - Add Email +
diff --git a/src/app/features/settings/account-settings/components/add-email/add-email.component.ts b/src/app/features/settings/account-settings/components/add-email/add-email.component.ts index 6482fecc0..7ed228484 100644 --- a/src/app/features/settings/account-settings/components/add-email/add-email.component.ts +++ b/src/app/features/settings/account-settings/components/add-email/add-email.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { InputText } from 'primeng/inputtext'; @@ -11,7 +13,7 @@ import { AddEmail } from '@osf/features/settings/account-settings/store/account- @Component({ selector: 'osf-add-email', - imports: [InputText, ReactiveFormsModule, Button], + imports: [InputText, ReactiveFormsModule, Button, TranslatePipe], templateUrl: './add-email.component.html', styleUrl: './add-email.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html index 1b68cdf79..69d6d3224 100644 --- a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html @@ -1,11 +1,15 @@

passwordForm.get(AccountSettingsPasswordFormControls.OldPassword)?.errors?.['required'] && passwordForm.get(AccountSettingsPasswordFormControls.OldPassword)?.touched ) { - Old password is required + {{ + 'settings.accountSettings.changePassword.validation.oldPasswordRequired' | translate + }} }
- +

}" /> - Your password needs to be at least 8 characters long, include both lower- and upper-case characters, and have - at least one number or special character + {{ 'settings.accountSettings.changePassword.form.passwordRequirements' | translate }} @if ( passwordForm.get(AccountSettingsPasswordFormControls.NewPassword)?.errors?.['required'] && passwordForm.get(AccountSettingsPasswordFormControls.NewPassword)?.touched ) { - New password is required + {{ + 'settings.accountSettings.changePassword.validation.newPasswordRequired' | translate + }} } @if ( passwordForm.errors?.['sameAsOldPassword'] && passwordForm.get(AccountSettingsPasswordFormControls.NewPassword)?.touched ) { - New password must be different from old password + {{ + 'settings.accountSettings.changePassword.validation.sameAsOldPassword' | translate + }} }
- +
@@ -84,7 +99,9 @@ {{ errorMessage() }} }
diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.scss b/src/app/features/settings/account-settings/components/change-password/change-password.component.scss index 3657ecb76..780b45db8 100644 --- a/src/app/features/settings/account-settings/components/change-password/change-password.component.scss +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.scss @@ -4,7 +4,7 @@ :host { @extend .account-setting; - label { + .password-label { color: var.$dark-blue-1; } diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.ts b/src/app/features/settings/account-settings/components/change-password/change-password.component.ts index 8f784b148..391b4e47a 100644 --- a/src/app/features/settings/account-settings/components/change-password/change-password.component.ts +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.ts @@ -1,3 +1,5 @@ +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { Password } from 'primeng/password'; @@ -17,17 +19,19 @@ import { AccountSettingsPasswordForm, AccountSettingsPasswordFormControls, } from '@osf/features/settings/account-settings/account.settings.entities'; -import { AccountSettingsService } from '@osf/features/settings/account-settings/services/account-settings.service'; + +import { AccountSettingsService } from '../../services'; @Component({ selector: 'osf-change-password', - imports: [ReactiveFormsModule, Password, CommonModule, Button], + imports: [ReactiveFormsModule, Password, CommonModule, Button, TranslatePipe], templateUrl: './change-password.component.html', styleUrl: './change-password.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChangePasswordComponent implements OnInit { readonly #accountSettingsService = inject(AccountSettingsService); + readonly #translateService = inject(TranslateService); readonly passwordForm: AccountSettingsPasswordForm = new FormGroup({ [AccountSettingsPasswordFormControls.OldPassword]: new FormControl('', { nonNullable: true, @@ -108,7 +112,9 @@ export class ChangePasswordComponent implements OnInit { if (error.error?.errors?.[0]?.detail) { this.errorMessage.set(error.error.errors[0].detail); } else { - this.errorMessage.set('Failed to update password. Please try again.'); + this.errorMessage.set( + this.#translateService.instant('settings.accountSettings.changePassword.messages.error') + ); } }, }); diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html index 568c95b36..11d189634 100644 --- a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html @@ -1,14 +1,15 @@ diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts index c7ef4982f..5b1be936b 100644 --- a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ProgressSpinner } from 'primeng/progressspinner'; @@ -17,7 +19,7 @@ import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; @Component({ selector: 'osf-connected-emails', - imports: [Button, ProgressSpinner, Skeleton], + imports: [Button, ProgressSpinner, Skeleton, TranslatePipe], templateUrl: './connected-emails.component.html', styleUrl: './connected-emails.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -26,6 +28,7 @@ export class ConnectedEmailsComponent { readonly #store = inject(Store); readonly #accountSettingsService = inject(AccountSettingsService); readonly #dialogService = inject(DialogService); + readonly #translateService = inject(TranslateService); readonly isMobile = toSignal(inject(IS_XSMALL)); protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); @@ -48,7 +51,7 @@ export class ConnectedEmailsComponent { this.dialogRef = this.#dialogService.open(AddEmailComponent, { width: '448px', focusOnShow: false, - header: 'Add alternative email', + header: this.#translateService.instant('settings.accountSettings.connectedEmails.dialog.title'), closeOnEscape: true, modal: true, closable: true, diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html index a53659cc6..732cb0488 100644 --- a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html @@ -1,8 +1,8 @@
diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts index e853bf35f..b9cec9b7f 100644 --- a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { DeleteExternalIdentity } from '@osf/features/settings/account-settings/store/account-settings.actions'; @@ -7,7 +9,7 @@ import { AccountSettingsSelectors } from '@osf/features/settings/account-setting @Component({ selector: 'osf-connected-identities', - imports: [], + imports: [TranslatePipe], templateUrl: './connected-identities.component.html', styleUrl: './connected-identities.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.html b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.html index 662839394..239af46f3 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.html +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.html @@ -1,16 +1,29 @@
-

Are you sure you want to request account deactivation?

-

This will preserve your account status

+ +
- - Cancel - + - - Undo deactivation request - +
diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.scss b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.scss index 7d1f2ad8e..b2606ff4d 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.scss +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.scss @@ -1,7 +1,5 @@ +@use "../../../../account-settings.component.scss" as account-settings; + :host { - h3, - p { - text-transform: none; - font-weight: normal; - } + @extend .account-setting; } diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.ts b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.ts index afd5e6917..94b1182a7 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.ts +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/cancel-deactivation/cancel-deactivation.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -9,7 +11,7 @@ import { CancelDeactivationRequest } from '@osf/features/settings/account-settin @Component({ selector: 'osf-cancel-deactivation', - imports: [Button], + imports: [Button, TranslatePipe], templateUrl: './cancel-deactivation.component.html', styleUrl: './cancel-deactivation.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.html b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.html index 8ec438c15..017d79bc6 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.html +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.html @@ -1,16 +1,29 @@
-

Are you sure you want to request account deactivation?

-

- An OSF administrator will review your request. If accepted, you will NOT be able to reactivate your account. +

+
- - Cancel - + - Configure +
diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.scss b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.scss index 7d1f2ad8e..b2606ff4d 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.scss +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.scss @@ -1,7 +1,5 @@ +@use "../../../../account-settings.component.scss" as account-settings; + :host { - h3, - p { - text-transform: none; - font-weight: normal; - } + @extend .account-setting; } diff --git a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.ts b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.ts index 3a5c07a62..8bb8ed345 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.ts +++ b/src/app/features/settings/account-settings/components/deactivate-account/components/deactivation-warning/deactivation-warning.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -9,7 +11,7 @@ import { DeactivateAccount } from '@osf/features/settings/account-settings/store @Component({ selector: 'osf-deactivation-warning', - imports: [Button], + imports: [Button, TranslatePipe], templateUrl: './deactivation-warning.component.html', styleUrl: './deactivation-warning.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html index fd761b712..d5c9f23d6 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html @@ -1,30 +1,33 @@ diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss index 6863c21d0..3f1e57889 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss @@ -1,12 +1,5 @@ @use "../../account-settings.component.scss" as account-settings; -@use "assets/styles/variables" as var; :host { @extend .account-setting; - - h3, - p { - font-weight: 400; - text-transform: none; - } } diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts index 38bdc9aa7..9774d9a53 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; import { Message } from 'primeng/message'; @@ -13,7 +15,7 @@ import { AccountSettingsSelectors } from '@osf/features/settings/account-setting @Component({ selector: 'osf-deactivate-account', - imports: [Button, Message, NgOptimizedImage], + imports: [Button, Message, NgOptimizedImage, TranslatePipe], templateUrl: './deactivate-account.component.html', styleUrl: './deactivate-account.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -22,13 +24,14 @@ export class DeactivateAccountComponent { #store = inject(Store); dialogRef: DynamicDialogRef | null = null; readonly #dialogService = inject(DialogService); + readonly #translateService = inject(TranslateService); protected accountSettings = this.#store.selectSignal(AccountSettingsSelectors.getAccountSettings); deactivateAccount() { this.dialogRef = this.#dialogService.open(DeactivationWarningComponent, { width: '552px', focusOnShow: false, - header: 'Deactivate account', + header: this.#translateService.instant('settings.accountSettings.deactivateAccount.dialog.deactivate.title'), closeOnEscape: true, modal: true, closable: true, @@ -39,7 +42,7 @@ export class DeactivateAccountComponent { this.dialogRef = this.#dialogService.open(CancelDeactivationComponent, { width: '552px', focusOnShow: false, - header: 'Undo deactivation request?', + header: this.#translateService.instant('settings.accountSettings.deactivateAccount.dialog.undo.title'), closeOnEscape: true, modal: true, closable: true, diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html index 2330dece2..06cb8bced 100644 --- a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html @@ -1,8 +1,8 @@ diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts index 94e6feb78..9879e1b92 100644 --- a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { Select } from 'primeng/select'; @@ -8,14 +10,14 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { UserSelectors } from '@core/store/user/user.selectors'; -import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; +import { Region } from '@osf/features/settings/account-settings/models/osf-models/region.model'; import { UpdateRegion } from '@osf/features/settings/account-settings/store/account-settings.actions'; import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; @Component({ selector: 'osf-default-storage-location', - imports: [Button, Select, FormsModule], + imports: [Button, Select, FormsModule, TranslatePipe], templateUrl: './default-storage-location.component.html', styleUrl: './default-storage-location.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/settings/account-settings/components/share-indexing/enums/share-indexing.enum.ts b/src/app/features/settings/account-settings/components/share-indexing/enums/share-indexing.enum.ts new file mode 100644 index 000000000..ea9e237aa --- /dev/null +++ b/src/app/features/settings/account-settings/components/share-indexing/enums/share-indexing.enum.ts @@ -0,0 +1,5 @@ +export enum ShareIndexingEnum { + None = -1, + OutOf = 0, + OptIn = 1, +} diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html index 658382a91..88c0cb0f8 100644 --- a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html @@ -1,29 +1,26 @@ diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts index bd804e71f..ea4f7d50b 100644 --- a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { RadioButton } from 'primeng/radiobutton'; @@ -7,25 +9,26 @@ import { ChangeDetectionStrategy, Component, effect, inject, signal } from '@ang import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { UserSelectors } from '@core/store/user/user.selectors'; +import { ShareIndexingEnum } from '@osf/features/settings/account-settings/components/share-indexing/enums/share-indexing.enum'; import { UpdateIndexing } from '@osf/features/settings/account-settings/store/account-settings.actions'; @Component({ selector: 'osf-share-indexing', - imports: [Button, RadioButton, ReactiveFormsModule, FormsModule], + imports: [Button, RadioButton, ReactiveFormsModule, FormsModule, TranslatePipe], templateUrl: './share-indexing.component.html', styleUrl: './share-indexing.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ShareIndexingComponent { readonly #store = inject(Store); - protected indexing = signal(''); + protected indexing = signal(ShareIndexingEnum.None); protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); updateIndexing = () => { if (this.currentUser()?.id) { - if (this.indexing() === '1') { + if (this.indexing() === ShareIndexingEnum.OptIn) { this.#store.dispatch(new UpdateIndexing(true)); - } else if (this.indexing() === '0') { + } else if (this.indexing() === ShareIndexingEnum.OutOf) { this.#store.dispatch(new UpdateIndexing(false)); } } @@ -35,9 +38,9 @@ export class ShareIndexingComponent { effect(() => { const user = this.currentUser(); if (user?.allowIndexing) { - this.indexing.set('1'); + this.indexing.set(ShareIndexingEnum.OptIn); } else if (user?.allowIndexing === false) { - this.indexing.set('0'); + this.indexing.set(ShareIndexingEnum.OutOf); } }); } diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html index 91bfe5b88..375cccbc9 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html @@ -1,17 +1,28 @@
-

Configuring two-factor authentication will not immediately activate this feature for your account.

-

- You will need to follow the steps that appear below to complete the activation of two-factor authentication for - your account. +

+
- - Cancel - + - Configure +
diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss index 7d1f2ad8e..b2606ff4d 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss @@ -1,7 +1,5 @@ +@use "../../../../account-settings.component.scss" as account-settings; + :host { - h3, - p { - text-transform: none; - font-weight: normal; - } + @extend .account-setting; } diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts index 64c8c1710..791aa8614 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts @@ -1,17 +1,19 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-models/account-settings.model'; import { EnableTwoFactorAuth } from '@osf/features/settings/account-settings/store/account-settings.actions'; @Component({ selector: 'osf-configure-two-factor', - imports: [Button, FormsModule], + imports: [Button, FormsModule, TranslatePipe], templateUrl: './configure-two-factor.component.html', styleUrl: './configure-two-factor.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/index.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/index.ts new file mode 100644 index 000000000..d0dcc5236 --- /dev/null +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/index.ts @@ -0,0 +1,2 @@ +export * from './configure-two-factor/configure-two-factor.component'; +export * from './verify-two-factor/verify-two-factor.component'; diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html index a321762b0..38fc920a7 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html @@ -1,10 +1,23 @@
-

Are you sure you want to disable two-factor authentication?

+
- - Cancel - + - Disable +
diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss index 7d1f2ad8e..b2606ff4d 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss @@ -1,7 +1,5 @@ +@use "../../../../account-settings.component.scss" as account-settings; + :host { - h3, - p { - text-transform: none; - font-weight: normal; - } + @extend .account-setting; } diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts index 694bf7ea0..edac5242e 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -9,7 +11,7 @@ import { DisableTwoFactorAuth } from '@osf/features/settings/account-settings/st @Component({ selector: 'osf-verify-two-factor', - imports: [Button], + imports: [Button, TranslatePipe], templateUrl: './verify-two-factor.component.html', styleUrl: './verify-two-factor.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html index 895cdfc43..6ba2ca2c4 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html @@ -1,30 +1,30 @@ @if (this.accountSettings()) { diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss index 3d073ac25..c508e9f25 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss @@ -4,13 +4,8 @@ :host { @extend .account-setting; - h3, - p { - font-weight: 400; - text-transform: none; - } - - .token { + ::ng-deep .token { color: var.$red-1; + display: inline; } } diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts index 77c679731..a05164645 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; import { InputText } from 'primeng/inputtext'; @@ -9,21 +11,24 @@ import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@a import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { UserSelectors } from '@core/store/user/user.selectors'; -import { ConfigureTwoFactorComponent } from '@osf/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component'; -import { VerifyTwoFactorComponent } from '@osf/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component'; -import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; -import { AccountSettingsService } from '@osf/features/settings/account-settings/services/account-settings.service'; +import { + ConfigureTwoFactorComponent, + VerifyTwoFactorComponent, +} from '@osf/features/settings/account-settings/components/two-factor-auth/components'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-models/account-settings.model'; import { DisableTwoFactorAuth, SetAccountSettings, } from '@osf/features/settings/account-settings/store/account-settings.actions'; import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors'; +import { AccountSettingsService } from '../../services'; + import { QRCodeComponent } from 'angularx-qrcode'; @Component({ selector: 'osf-two-factor-auth', - imports: [Button, QRCodeComponent, ReactiveFormsModule, InputText], + imports: [Button, QRCodeComponent, ReactiveFormsModule, InputText, TranslatePipe], templateUrl: './two-factor-auth.component.html', styleUrl: './two-factor-auth.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -33,6 +38,7 @@ export class TwoFactorAuthComponent { dialogRef: DynamicDialogRef | null = null; readonly #dialogService = inject(DialogService); readonly #accountSettingsService = inject(AccountSettingsService); + readonly #translateService = inject(TranslateService); readonly accountSettings = this.#store.selectSignal(AccountSettingsSelectors.getAccountSettings); readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); @@ -47,12 +53,11 @@ export class TwoFactorAuthComponent { errorMessage = signal(''); - // open dialog configureTwoFactorAuth(): void { this.dialogRef = this.#dialogService.open(ConfigureTwoFactorComponent, { width: '520px', focusOnShow: false, - header: 'Configure', + header: this.#translateService.instant('settings.accountSettings.twoFactorAuth.dialog.configure.title'), closeOnEscape: true, modal: true, closable: true, @@ -64,7 +69,7 @@ export class TwoFactorAuthComponent { this.dialogRef = this.#dialogService.open(VerifyTwoFactorComponent, { width: '520px', focusOnShow: false, - header: 'Disable', + header: this.#translateService.instant('settings.accountSettings.twoFactorAuth.dialog.disable.title'), closeOnEscape: true, modal: true, closable: true, @@ -80,7 +85,9 @@ export class TwoFactorAuthComponent { if (error.error?.errors?.[0]?.detail) { this.errorMessage.set(error.error.errors[0].detail); } else { - this.errorMessage.set('Verification code is invalid. Please try again.'); + this.errorMessage.set( + this.#translateService.instant('settings.accountSettings.twoFactorAuth.verification.error') + ); } }, }); diff --git a/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts b/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts index f8751d4e0..e701b9f9b 100644 --- a/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts @@ -1,5 +1,5 @@ import { ApiData } from '@core/services/json-api/json-api.entity'; -import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-models/account-settings.model'; import { AccountSettingsResponse } from '@osf/features/settings/account-settings/models/responses/get-account-settings-response.entity'; export function MapAccountSettings(data: ApiData): AccountSettings { diff --git a/src/app/features/settings/account-settings/mappers/emails.mapper.ts b/src/app/features/settings/account-settings/mappers/emails.mapper.ts index c44562cd1..53fa02cab 100644 --- a/src/app/features/settings/account-settings/mappers/emails.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/emails.mapper.ts @@ -1,5 +1,5 @@ import { ApiData } from '@core/services/json-api/json-api.entity'; -import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-entities/account-email.entity'; +import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-models/account-email.model'; import { AccountEmailResponse } from '@osf/features/settings/account-settings/models/responses/list-emails.entity'; export function MapEmails(emails: ApiData[]): AccountEmail[] { diff --git a/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts b/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts index 924207de1..c6afe5e03 100644 --- a/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts @@ -1,5 +1,5 @@ import { ApiData } from '@core/services/json-api/json-api.entity'; -import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-entities/external-institution.entity'; +import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-models/external-institution.model'; import { ExternalIdentityResponse } from '@osf/features/settings/account-settings/models/responses/list-identities-response.entity'; export function MapExternalIdentities(data: ApiData[]): ExternalIdentity[] { diff --git a/src/app/features/settings/account-settings/mappers/index.ts b/src/app/features/settings/account-settings/mappers/index.ts new file mode 100644 index 000000000..95afda414 --- /dev/null +++ b/src/app/features/settings/account-settings/mappers/index.ts @@ -0,0 +1,4 @@ +export * from './account-settings.mapper'; +export * from './emails.mapper'; +export * from './external-identities.mapper'; +export * from './regions.mapper'; diff --git a/src/app/features/settings/account-settings/mappers/regions.mapper.ts b/src/app/features/settings/account-settings/mappers/regions.mapper.ts index ac790567d..ff679d0ff 100644 --- a/src/app/features/settings/account-settings/mappers/regions.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/regions.mapper.ts @@ -1,5 +1,5 @@ import { ApiData } from '@core/services/json-api/json-api.entity'; -import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; +import { Region } from '@osf/features/settings/account-settings/models/osf-models/region.model'; export function MapRegions(data: ApiData<{ name: string }, null, null>[]): Region[] { const regions: Region[] = []; diff --git a/src/app/features/settings/account-settings/models/index.ts b/src/app/features/settings/account-settings/models/index.ts new file mode 100644 index 000000000..9afc872a1 --- /dev/null +++ b/src/app/features/settings/account-settings/models/index.ts @@ -0,0 +1,9 @@ +export * from './osf-models/account-email.model'; +export * from './osf-models/account-settings.model'; +export * from './osf-models/external-institution.model'; +export * from './osf-models/region.model'; +export * from './responses/get-account-settings-response.entity'; +export * from './responses/get-email-response.entity'; +export * from './responses/get-regions-response.entity'; +export * from './responses/list-emails.entity'; +export * from './responses/list-identities-response.entity'; diff --git a/src/app/features/settings/account-settings/models/osf-entities/account-email.entity.ts b/src/app/features/settings/account-settings/models/osf-models/account-email.model.ts similarity index 100% rename from src/app/features/settings/account-settings/models/osf-entities/account-email.entity.ts rename to src/app/features/settings/account-settings/models/osf-models/account-email.model.ts diff --git a/src/app/features/settings/account-settings/models/osf-entities/account-settings.entity.ts b/src/app/features/settings/account-settings/models/osf-models/account-settings.model.ts similarity index 100% rename from src/app/features/settings/account-settings/models/osf-entities/account-settings.entity.ts rename to src/app/features/settings/account-settings/models/osf-models/account-settings.model.ts diff --git a/src/app/features/settings/account-settings/models/osf-entities/external-institution.entity.ts b/src/app/features/settings/account-settings/models/osf-models/external-institution.model.ts similarity index 100% rename from src/app/features/settings/account-settings/models/osf-entities/external-institution.entity.ts rename to src/app/features/settings/account-settings/models/osf-models/external-institution.model.ts diff --git a/src/app/features/settings/account-settings/models/osf-entities/region.entity.ts b/src/app/features/settings/account-settings/models/osf-models/region.model.ts similarity index 100% rename from src/app/features/settings/account-settings/models/osf-entities/region.entity.ts rename to src/app/features/settings/account-settings/models/osf-models/region.model.ts diff --git a/src/app/features/settings/account-settings/services/account-settings.service.ts b/src/app/features/settings/account-settings/services/account-settings.service.ts index f3c7b86d5..aed1af89d 100644 --- a/src/app/features/settings/account-settings/services/account-settings.service.ts +++ b/src/app/features/settings/account-settings/services/account-settings.service.ts @@ -10,24 +10,21 @@ import { mapUserUStoUser } from '@core/services/mappers/users/users.mapper'; import { User } from '@core/services/user/user.entity'; import { UserSelectors } from '@core/store/user/user.selectors'; import { ApiData, JsonApiResponse } from '@osf/core/services/json-api/json-api.entity'; -import { MapAccountSettings } from '@osf/features/settings/account-settings/mappers/account-settings.mapper'; -import { MapEmail, MapEmails } from '@osf/features/settings/account-settings/mappers/emails.mapper'; -import { MapExternalIdentities } from '@osf/features/settings/account-settings/mappers/external-identities.mapper'; -import { MapRegions } from '@osf/features/settings/account-settings/mappers/regions.mapper'; -import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-entities/account-email.entity'; -import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; -import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-entities/external-institution.entity'; -import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; -import { GetAccountSettingsResponse } from '@osf/features/settings/account-settings/models/responses/get-account-settings-response.entity'; -import { GetEmailResponse } from '@osf/features/settings/account-settings/models/responses/get-email-response.entity'; -import { GetRegionsResponse } from '@osf/features/settings/account-settings/models/responses/get-regions-response.entity'; + +import { environment } from '../../../../../environments/environment'; +import { MapAccountSettings, MapEmail, MapEmails, MapExternalIdentities, MapRegions } from '../mappers'; import { + AccountEmail, AccountEmailResponse, + AccountSettings, + ExternalIdentity, + GetAccountSettingsResponse, + GetEmailResponse, + GetRegionsResponse, ListEmailsResponse, -} from '@osf/features/settings/account-settings/models/responses/list-emails.entity'; -import { ListIdentitiesResponse } from '@osf/features/settings/account-settings/models/responses/list-identities-response.entity'; - -import { environment } from '../../../../../environments/environment'; + ListIdentitiesResponse, + Region, +} from '../models'; @Injectable({ providedIn: 'root', diff --git a/src/app/features/settings/account-settings/services/index.ts b/src/app/features/settings/account-settings/services/index.ts new file mode 100644 index 000000000..f559ff15a --- /dev/null +++ b/src/app/features/settings/account-settings/services/index.ts @@ -0,0 +1 @@ +export * from './account-settings.service'; diff --git a/src/app/features/settings/account-settings/store/account-settings.actions.ts b/src/app/features/settings/account-settings/store/account-settings.actions.ts index 1d6676fd5..32a9a28c9 100644 --- a/src/app/features/settings/account-settings/store/account-settings.actions.ts +++ b/src/app/features/settings/account-settings/store/account-settings.actions.ts @@ -1,4 +1,4 @@ -import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-models/account-settings.model'; export class GetEmails { static readonly type = '[AccountSettings] Get Emails'; diff --git a/src/app/features/settings/account-settings/store/account-settings.model.ts b/src/app/features/settings/account-settings/store/account-settings.model.ts index 1243cce9c..8e6261074 100644 --- a/src/app/features/settings/account-settings/store/account-settings.model.ts +++ b/src/app/features/settings/account-settings/store/account-settings.model.ts @@ -1,8 +1,8 @@ import { Institution } from '@osf/features/institutions/entities/institutions.models'; -import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-entities/account-email.entity'; -import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; -import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-entities/external-institution.entity'; -import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; +import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-models/account-email.model'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-models/account-settings.model'; +import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-models/external-institution.model'; +import { Region } from '@osf/features/settings/account-settings/models/osf-models/region.model'; export interface AccountSettingsStateModel { emails: AccountEmail[]; diff --git a/src/app/features/settings/account-settings/store/account-settings.selectors.ts b/src/app/features/settings/account-settings/store/account-settings.selectors.ts index f39380270..6bbcc67f2 100644 --- a/src/app/features/settings/account-settings/store/account-settings.selectors.ts +++ b/src/app/features/settings/account-settings/store/account-settings.selectors.ts @@ -1,10 +1,10 @@ import { Selector } from '@ngxs/store'; import { Institution } from '@osf/features/institutions/entities/institutions.models'; -import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-entities/account-email.entity'; -import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-entities/account-settings.entity'; -import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-entities/external-institution.entity'; -import { Region } from '@osf/features/settings/account-settings/models/osf-entities/region.entity'; +import { AccountEmail } from '@osf/features/settings/account-settings/models/osf-models/account-email.model'; +import { AccountSettings } from '@osf/features/settings/account-settings/models/osf-models/account-settings.model'; +import { ExternalIdentity } from '@osf/features/settings/account-settings/models/osf-models/external-institution.model'; +import { Region } from '@osf/features/settings/account-settings/models/osf-models/region.model'; import { AccountSettingsStateModel } from '@osf/features/settings/account-settings/store/account-settings.model'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; diff --git a/src/app/features/settings/account-settings/store/account-settings.state.ts b/src/app/features/settings/account-settings/store/account-settings.state.ts index a62301fd1..ad696c880 100644 --- a/src/app/features/settings/account-settings/store/account-settings.state.ts +++ b/src/app/features/settings/account-settings/store/account-settings.state.ts @@ -6,7 +6,6 @@ import { inject, Injectable } from '@angular/core'; import { SetCurrentUser } from '@core/store/user'; import { InstitutionsService } from '@osf/features/institutions/institutions.service'; -import { AccountSettingsService } from '@osf/features/settings/account-settings/services/account-settings.service'; import { AddEmail, CancelDeactivationRequest, @@ -30,6 +29,8 @@ import { } from '@osf/features/settings/account-settings/store/account-settings.actions'; import { AccountSettingsStateModel } from '@osf/features/settings/account-settings/store/account-settings.model'; +import { AccountSettingsService } from '../services'; + @Injectable() @State({ name: 'accountSettings', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ae8c84dd1..42b48d954 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -461,6 +461,158 @@ "discardChanges": "Discard Changes", "save": "Save" } + }, + "accountSettings": { + "common": { + "buttons": { + "cancel": "Cancel", + "configure": "Configure", + "disable": "Disable", + "enable": "Enable", + "undo": "Undo", + "request": "Request" + } + }, + "deactivateAccount": { + "title": "Deactivate Account", + "warning": { + "title": "Warning: This action cannot be undone once approved.", + "confirm": "Are you sure you want to request account deactivation?", + "description": "An OSF administrator will review your request. If accepted, you will NOT be able to reactivate your account." + }, + "description": { + "main": "Deactivating your account will remove you from all public projects to which you are a contributor. Your account will no longer be associated with OSF projects, and your work on the OSF will be inaccessible.", + "secondary": "If this is a secondary account that you want to close, consider merging your accounts" + }, + "pendingDeactivation": "Your account is currently pending deactivation.", + "actions": { + "undoDeactivation": "Undo deactivation request", + "requestDeactivation": "Request deactivation" + }, + "dialog": { + "deactivate": { + "title": "Deactivate account" + }, + "undo": { + "title": "Undo deactivation request?" + } + } + }, + "affiliatedInstitutions": { + "title": "Affiliated Institutions", + "description": "Connect your account to institutions to access institutional features and services.", + "noInstitutions": "You have no affiliations." + }, + "addEmail": { + "title": "Add alternative email", + "description": "Add an alternative email address to your account.", + "form": { + "email": "Email", + "emailPlaceholder": "email@example.com" + }, + "buttons": { + "cancel": "Cancel", + "add": "Add" + }, + "messages": { + "success": "Alternative email added successfully", + "error": "Failed to add alternative email" + } + }, + "changePassword": { + "title": "Change Password", + "form": { + "oldPassword": "Old password", + "newPassword": "New password", + "confirmPassword": "Confirm password", + "passwordRequirements": "Your password needs to be at least 8 characters long, include both lower- and upper-case characters, and have at least one number or special character" + }, + "validation": { + "oldPasswordRequired": "Old password is required", + "newPasswordRequired": "New password is required", + "confirmPasswordRequired": "Please confirm your password", + "passwordsDoNotMatch": "Passwords do not match", + "sameAsOldPassword": "New password must be different from old password" + }, + "buttons": { + "update": "Update" + }, + "messages": { + "error": "Failed to update password. Please try again." + } + }, + "connectedEmails": { + "title": "Connected Emails", + "description": "To merge an existing account with this one or to log in with multiple email addresses, add an alternate email address below. All projects and components will be displayed under the email address listed as primary.", + "primaryEmail": "Primary Email:", + "alternateEmails": "Alternate Emails:", + "unconfirmedEmails": "Unconfirmed emails:", + "buttons": { + "makePrimary": "Make Primary", + "resend": "Resend", + "resendConfirmation": "Resend confirmation", + "addEmail": "Add Email" + }, + "dialog": { + "title": "Add alternative email" + } + }, + "connectedIdentities": { + "title": "Connected identities", + "description": "Connected identities allow you to log in to the OSF via a third-party service. You can revoke these identifies.", + "noIdentities": "You have not authorized any external services to log in to the OSF." + }, + "defaultStorageLocation": { + "title": "Default storage location", + "description": "This location will be applied to new projects and components. It will not affect existing projects and components.", + "buttons": { + "update": "Update Location" + } + }, + "shareIndexing": { + "title": "Opt out of SHARE indexing", + "description": "By default, OSF users are indexed into SHARE, a free, open dataset of research metadata. This allows SHARE to include your user profile and research in its database, which is used by search engines and other services to make research more discoverable. You can opt out of this indexing by checking the box below. NOTE: Public projects, files, registrations, and preprints will still be indexed in SHARE.", + "learnMore": "Learn more about SHARE", + "options": { + "optOut": "Out of SHARE Indexing", + "optIn": "Opt In To SHARE Indexing" + }, + "buttons": { + "update": "Update" + } + }, + "twoFactorAuth": { + "title": "Two-factor authentication", + "description": { + "enabled": "By using two-factor authentication, you will protect your OSF account with both your password and your mobile phone.", + "warning": "Important: If you lose access to your mobile device, you will not be able to log in to your OSF account.", + "setup": "To use, you must install an appropriate application on your mobile device. Google Authenticator is a popular choice and is available for both Android and iOS.", + "verification": "Once verified, your device will display a six-digit code that must be entered during the login process. This code changes every few seconds, which means that unauthorized users will not be able to log in to you account, even if they know your password.", + "scan": "Scan the image below, or enter the secret key

{{secret}}

into your authentication device.", + "disabled": "Two-factor authentication protects your OSF account using both your password and email." + }, + "verification": { + "label": "Enter your verification code:", + "error": "Verification code is invalid. Please try again." + }, + "dialog": { + "configure": { + "title": "Configure" + }, + "disable": { + "title": "Disable" + } + }, + "configure": { + "description": { + "main": "Configuring two-factor authentication will not immediately activate this feature for your account.", + "steps": "You will need to follow the steps that appear below to complete the activation of two-factor authentication for your account." + } + }, + "verify": { + "title": "Are you sure you want to disable two-factor authentication?" + } + } } }, "footer": { @@ -482,4 +634,4 @@ }, "copyright": "Copyright © 2011-2025" } -} +} \ No newline at end of file diff --git a/src/assets/icons/source/arrow-down.svg b/src/assets/icons/source/arrow-down.svg index 44f309f17..9adabdd7f 100644 --- a/src/assets/icons/source/arrow-down.svg +++ b/src/assets/icons/source/arrow-down.svg @@ -1,5 +1,3 @@ - - - + + - From 6618bd7b443fecc6ee5a8a5730045a114e2a81d6 Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Wed, 14 May 2025 12:09:04 +0300 Subject: [PATCH 3/4] fix(account-settings): translations fixes --- .../components/breadcrumb/breadcrumb.component.html | 2 +- .../confirm-email/confirm-email.component.html | 10 ++++++---- .../confirm-email/confirm-email.component.ts | 4 +++- src/app/features/home/home.component.ts | 2 +- src/assets/i18n/en.json | 9 +++++++++ 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/app/core/components/breadcrumb/breadcrumb.component.html b/src/app/core/components/breadcrumb/breadcrumb.component.html index 52e6b0d74..f34f2ce67 100644 --- a/src/app/core/components/breadcrumb/breadcrumb.component.html +++ b/src/app/core/components/breadcrumb/breadcrumb.component.html @@ -1,4 +1,4 @@ -@if (!parsedUrl()[0].includes('home')) { +@if (!parsedUrl()[0].includes('home') && !parsedUrl()[0].includes('confirm')) {