diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b2c8f0..c1d822c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added -- Users: Added `signUp` use case for creating a builtin user account without authentication (`POST /users/signup`). -- Users: Added `getOrcidSignUpAuthorizationUrl` and `connectOrcidForSignUp` use cases for authenticating an ORCID iD while signing up. -- Users: Added optional `authenticatedOrcid` to `AuthenticatedUser`. -- Core: `ReadError` and `WriteError` now expose the response's `statusCode` and, when the API returns them, per-field `fieldErrors`. - ### Changed -- Core: `ApiRepository.doPost` accepts an optional `authRequired` argument (default `true`) to send a request without the configured credentials. - ### Fixed ### Removed diff --git a/docs/useCases.md b/docs/useCases.md index 72ef6fbe..64a2c8d6 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -109,13 +109,10 @@ The different use cases currently available in the package are classified below, - [Users read use cases](#users-read-use-cases) - [Get Current Authenticated User](#get-current-authenticated-user) - [Get Current API Token](#get-current-api-token) - - [Get ORCID Sign-Up Authorization URL](#get-orcid-sign-up-authorization-url) - [Users write use cases](#users-write-use-cases) - [Delete Current API Token](#delete-current-api-token) - [Recreate Current API Token](#recreate-current-api-token) - [Register User](#register-user) - - [Sign Up](#sign-up) - - [Connect ORCID For Sign-Up](#connect-orcid-for-sign-up) - [Roles](#Roles) - [Roles read use cases](#roles-read-use-cases) - [Get User Selectable Roles](#get-user-selectable-roles) @@ -2750,8 +2747,6 @@ There is a third optional parameter called `datasetType` which will include addi Returns the current [AuthenticatedUser](../src/users/domain/models/AuthenticatedUser.ts) corresponding to the authentication mechanism provided through `ApiConfig`. -The `authenticatedOrcid` property holds the URL of the ORCID iD the user has authenticated and linked to their account, if any. - ##### Example call: ```typescript @@ -2788,32 +2783,6 @@ getCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfo) => { _See [use case](../src/users/domain/useCases/GetCurrentApiToken.ts) implementation_. -#### Get ORCID Sign-Up Authorization URL - -Returns the ORCID authorization URL that sends a new user to ORCID to authenticate their ORCID iD while signing up. Send the user's browser to this URL; after they sign in, ORCID redirects back to `redirectUri` with `code` and `state` query parameters, which are then passed to [Connect ORCID For Sign-Up](#connect-orcid-for-sign-up). - -No credentials are sent, so this works for anonymous users. The installation needs an ORCID OAuth2 provider (it can be disabled for login), and `redirectUri` must be listed in its `dataverse.api.signup.orcid-redirect-uris` setting. - -##### Example call: - -```typescript -import { getOrcidSignUpAuthorizationUrl } from '@iqss/dataverse-client-javascript' - -/* ... */ - -const redirectUri = 'https://dataverse.example.edu/spa/sign-up' - -getOrcidSignUpAuthorizationUrl.execute(redirectUri).then((authorizationUrl: string) => { - window.location.assign(authorizationUrl) -}) - -/* ... */ -``` - -_See [use case](../src/users/domain/useCases/GetOrcidSignUpAuthorizationUrl.ts) implementation_. - -The use case throws a `ReadError` with `statusCode` 404 when no ORCID provider is configured, and 400 when the redirect URI is not allowed. - ### Users write use cases ### Delete Current API Token @@ -2882,81 +2851,6 @@ registerUser.execute(userDTO) _See [use case](../src/users/domain/useCases/RegisterUser.ts) implementation_. -### Sign Up - -Creates a new builtin user account, given a [SignUpDTO](../src/users/domain/dtos/SignUpDTO.ts). No credentials are sent, so this works for anonymous users. The installation must enable the `api-builtin-user-signup` feature flag and allow sign-up (`:AllowSignUp`). - -To save an authenticated ORCID iD to the new account, include the `orcidToken` returned by [Connect ORCID For Sign-Up](#connect-orcid-for-sign-up). - -##### Example call: - -```typescript -import { signUp } from '@iqss/dataverse-client-javascript' - -/* ... */ - -const signUpDTO: SignUpDTO = { - username: 'alovelace', - firstName: 'Ada', - lastName: 'Lovelace', - emailAddress: 'ada@example.edu', - password: 'Str0ngPassw0rd', - affiliation: 'Harvard University', - termsAccepted: true -} - -signUp - .execute(signUpDTO) - .then((result: SignUpResult) => { - if (result.status === 'created') { - /* the account exists; result.identifier is e.g. '@alovelace' */ - } else { - /* status 'pending': the installation sent a verification email first */ - } - }) - .catch((error: WriteError) => { - /* error.fieldErrors maps invalid fields to messages, e.g. { username: 'This username is already taken.' } */ - }) - -/* ... */ -``` - -_See [use case](../src/users/domain/useCases/SignUp.ts) implementation_. - -The [SignUpResult](../src/users/domain/models/SignUpResult.ts) is `{ status: 'created', identifier }` when the account was created right away, or `{ status: 'pending' }` when the installation requires email verification before creating it. - -### Connect ORCID For Sign-Up - -Exchanges the `code` and `state` that ORCID returns to the redirect URI for the user's authenticated ORCID iD, given an [OrcidSignUpCallbackDTO](../src/users/domain/dtos/OrcidSignUpCallbackDTO.ts). Use the same `redirectUri` that was passed to [Get ORCID Sign-Up Authorization URL](#get-orcid-sign-up-authorization-url). No credentials are sent. - -Returns an [OrcidSignUpConnection](../src/users/domain/models/OrcidSignUpConnection.ts) with the ORCID iD, any details the user shares on their ORCID record (useful to pre-fill a sign-up form), and an `orcidToken` to include in [Sign Up](#sign-up). The token is valid for 30 minutes. - -##### Example call: - -```typescript -import { connectOrcidForSignUp } from '@iqss/dataverse-client-javascript' - -/* ... */ - -const params = new URLSearchParams(window.location.search) - -connectOrcidForSignUp - .execute({ - code: params.get('code') as string, - state: params.get('state') as string, - redirectUri: 'https://dataverse.example.edu/spa/sign-up' - }) - .then((connection: OrcidSignUpConnection) => { - /* connection.orcid, connection.firstName, ..., connection.orcidToken */ - }) - -/* ... */ -``` - -_See [use case](../src/users/domain/useCases/ConnectOrcidForSignUp.ts) implementation_. - -The use case throws a `WriteError` with `statusCode` 409 when the ORCID iD is already linked to another account, and 400 when the state is invalid or expired. - ## Roles ### Get User Selectable Roles diff --git a/src/core/domain/repositories/ReadError.ts b/src/core/domain/repositories/ReadError.ts index f3e40509..67028c8b 100644 --- a/src/core/domain/repositories/ReadError.ts +++ b/src/core/domain/repositories/ReadError.ts @@ -1,7 +1,7 @@ -import { RepositoryError, RepositoryErrorDetails } from './RepositoryError' +import { RepositoryError } from './RepositoryError' export class ReadError extends RepositoryError { - constructor(reason?: string, details?: RepositoryErrorDetails) { - super('There was an error when reading the resource.', reason, details) + constructor(reason?: string) { + super('There was an error when reading the resource.', reason) } } diff --git a/src/core/domain/repositories/RepositoryError.ts b/src/core/domain/repositories/RepositoryError.ts index d29bebcf..3a5495a6 100644 --- a/src/core/domain/repositories/RepositoryError.ts +++ b/src/core/domain/repositories/RepositoryError.ts @@ -1,18 +1,8 @@ -export interface RepositoryErrorDetails { - statusCode?: number - fieldErrors?: Record -} - export abstract class RepositoryError extends Error { - readonly statusCode?: number - readonly fieldErrors?: Record - - constructor(message: string, reason?: string, details?: RepositoryErrorDetails) { + constructor(message: string, reason?: string) { if (reason) { message += ` Reason was: ${reason}` } super(message) - this.statusCode = details?.statusCode - this.fieldErrors = details?.fieldErrors } } diff --git a/src/core/domain/repositories/WriteError.ts b/src/core/domain/repositories/WriteError.ts index 9661f89b..2d00be6c 100644 --- a/src/core/domain/repositories/WriteError.ts +++ b/src/core/domain/repositories/WriteError.ts @@ -1,7 +1,7 @@ -import { RepositoryError, RepositoryErrorDetails } from './RepositoryError' +import { RepositoryError } from './RepositoryError' export class WriteError extends RepositoryError { - constructor(reason?: string, details?: RepositoryErrorDetails) { - super('There was an error when writing the resource.', reason, details) + constructor(reason?: string) { + super('There was an error when writing the resource.', reason) } } diff --git a/src/core/index.ts b/src/core/index.ts index a525bacc..ddd54a8a 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,6 +1,5 @@ export { ReadError } from './domain/repositories/ReadError' export { WriteError } from './domain/repositories/WriteError' -export { RepositoryErrorDetails } from './domain/repositories/RepositoryError' export { ApiConfig } from './infra/repositories/ApiConfig' export { DvObjectOwnerNode, DvObjectType } from './domain/models/DvObjectOwnerNode' export { PublicationStatus } from './domain/models/PublicationStatus' diff --git a/src/core/infra/repositories/ApiRepository.ts b/src/core/infra/repositories/ApiRepository.ts index 3c65fd22..7a70c17c 100644 --- a/src/core/infra/repositories/ApiRepository.ts +++ b/src/core/infra/repositories/ApiRepository.ts @@ -1,7 +1,6 @@ import axios, { AxiosResponse } from 'axios' import { ReadError } from '../../domain/repositories/ReadError' import { WriteError } from '../../domain/repositories/WriteError' -import { RepositoryErrorDetails } from '../../domain/repositories/RepositoryError' import { buildRequestConfig, buildRequestUrl } from './apiConfigBuilders' import { ApiConstants } from './ApiConstants' @@ -15,7 +14,7 @@ export abstract class ApiRepository { .get(buildRequestUrl(apiEndpoint), buildRequestConfig(authRequired, queryParams)) .then((response) => response) .catch((error) => { - throw new ReadError(this.buildErrorMessage(error), this.buildErrorDetails(error)) + throw new ReadError(this.buildErrorMessage(error)) }) } @@ -23,10 +22,9 @@ export abstract class ApiRepository { apiEndpoint: string, data: string | object | boolean, queryParams: object = {}, - contentType: string = ApiConstants.CONTENT_TYPE_APPLICATION_JSON, - authRequired = true + contentType: string = ApiConstants.CONTENT_TYPE_APPLICATION_JSON ): Promise { - return await this.doRequest('post', apiEndpoint, data, queryParams, contentType, authRequired) + return await this.doRequest('post', apiEndpoint, data, queryParams, contentType) } public async doPut( @@ -43,7 +41,7 @@ export abstract class ApiRepository { .delete(buildRequestUrl(apiEndpoint), buildRequestConfig(true, queryParams)) .then((response) => response) .catch((error) => { - throw new WriteError(this.buildErrorMessage(error), this.buildErrorDetails(error)) + throw new WriteError(this.buildErrorMessage(error)) }) } @@ -61,15 +59,6 @@ export abstract class ApiRepository { : `/${resourceName}${operationSegment}` } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private buildErrorDetails(error: any): RepositoryErrorDetails { - const fieldErrors = error?.response?.data?.fieldErrors - return { - statusCode: error?.response?.status, - fieldErrors: fieldErrors && typeof fieldErrors === 'object' ? fieldErrors : undefined - } - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private buildErrorMessage(error: any): string { const status = @@ -83,8 +72,7 @@ export abstract class ApiRepository { apiEndpoint: string, data: string | object | boolean, queryParams: object = {}, - contentType: string = ApiConstants.CONTENT_TYPE_APPLICATION_JSON, - authRequired = true + contentType: string = ApiConstants.CONTENT_TYPE_APPLICATION_JSON ): Promise { let requestData = data @@ -95,13 +83,13 @@ export abstract class ApiRepository { } const requestUrl = buildRequestUrl(apiEndpoint) - const requestConfig = buildRequestConfig(authRequired, queryParams, contentType) + const requestConfig = buildRequestConfig(true, queryParams, contentType) try { const response = await axios[method](requestUrl, requestData, requestConfig) return response } catch (error) { - throw new WriteError(this.buildErrorMessage(error), this.buildErrorDetails(error)) + throw new WriteError(this.buildErrorMessage(error)) } } } diff --git a/src/users/domain/dtos/OrcidSignUpCallbackDTO.ts b/src/users/domain/dtos/OrcidSignUpCallbackDTO.ts deleted file mode 100644 index b7c78250..00000000 --- a/src/users/domain/dtos/OrcidSignUpCallbackDTO.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface OrcidSignUpCallbackDTO { - code: string - state: string - redirectUri: string -} diff --git a/src/users/domain/dtos/SignUpDTO.ts b/src/users/domain/dtos/SignUpDTO.ts deleted file mode 100644 index b896c140..00000000 --- a/src/users/domain/dtos/SignUpDTO.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface SignUpDTO { - username: string - firstName: string - lastName: string - emailAddress: string - password: string - affiliation?: string - position?: string - termsAccepted: boolean - orcidToken?: string -} diff --git a/src/users/domain/models/AuthenticatedUser.ts b/src/users/domain/models/AuthenticatedUser.ts index b73fb10c..1fd0c475 100644 --- a/src/users/domain/models/AuthenticatedUser.ts +++ b/src/users/domain/models/AuthenticatedUser.ts @@ -16,5 +16,4 @@ export interface AuthenticatedUser { affiliation?: string position?: string emailLastConfirmed?: string - authenticatedOrcid?: string } diff --git a/src/users/domain/models/OrcidSignUpConnection.ts b/src/users/domain/models/OrcidSignUpConnection.ts deleted file mode 100644 index 69c126ad..00000000 --- a/src/users/domain/models/OrcidSignUpConnection.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface OrcidSignUpConnection { - orcid: string - orcidToken: string - firstName?: string - lastName?: string - emailAddress?: string - affiliation?: string - position?: string -} diff --git a/src/users/domain/models/SignUpResult.ts b/src/users/domain/models/SignUpResult.ts deleted file mode 100644 index ad484785..00000000 --- a/src/users/domain/models/SignUpResult.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type SignUpResult = SignUpCreatedResult | SignUpPendingResult - -export interface SignUpCreatedResult { - status: 'created' - identifier: string -} - -export interface SignUpPendingResult { - status: 'pending' -} diff --git a/src/users/domain/repositories/IUsersRepository.ts b/src/users/domain/repositories/IUsersRepository.ts index d33c29af..e2b649d7 100644 --- a/src/users/domain/repositories/IUsersRepository.ts +++ b/src/users/domain/repositories/IUsersRepository.ts @@ -1,10 +1,6 @@ import { ApiTokenInfo } from '../models/ApiTokenInfo' import { AuthenticatedUser } from '../models/AuthenticatedUser' import { UserDTO } from '../dtos/UserDTO' -import { SignUpDTO } from '../dtos/SignUpDTO' -import { OrcidSignUpCallbackDTO } from '../dtos/OrcidSignUpCallbackDTO' -import { SignUpResult } from '../models/SignUpResult' -import { OrcidSignUpConnection } from '../models/OrcidSignUpConnection' export interface IUsersRepository { getCurrentAuthenticatedUser(): Promise @@ -12,7 +8,4 @@ export interface IUsersRepository { getCurrentApiToken(): Promise deleteCurrentApiToken(): Promise registerUser(userDTO: UserDTO): Promise - signUp(signUpDTO: SignUpDTO): Promise - getOrcidSignUpAuthorizationUrl(redirectUri: string): Promise - connectOrcidForSignUp(callback: OrcidSignUpCallbackDTO): Promise } diff --git a/src/users/domain/useCases/ConnectOrcidForSignUp.ts b/src/users/domain/useCases/ConnectOrcidForSignUp.ts deleted file mode 100644 index c2701c4a..00000000 --- a/src/users/domain/useCases/ConnectOrcidForSignUp.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { UseCase } from '../../../core/domain/useCases/UseCase' -import { OrcidSignUpCallbackDTO } from '../dtos/OrcidSignUpCallbackDTO' -import { OrcidSignUpConnection } from '../models/OrcidSignUpConnection' -import { IUsersRepository } from '../repositories/IUsersRepository' - -export class ConnectOrcidForSignUp implements UseCase { - private usersRepository: IUsersRepository - - constructor(usersRepository: IUsersRepository) { - this.usersRepository = usersRepository - } - - /** - * Exchanges the code and state that ORCID returns to the redirect URI for the user's authenticated ORCID iD. - * No authentication is used. - * - * @param {OrcidSignUpCallbackDTO} [callback] - The code and state from ORCID's redirect, and the same redirectUri used to get the authorization URL. - * @returns {Promise} - The ORCID iD, any details the user shares on ORCID, and an orcidToken to include when signing up. - * @throws {WriteError} - If the state is invalid or expired, ORCID rejects the code, or the ORCID iD is already linked to another account (statusCode 409). - */ - async execute(callback: OrcidSignUpCallbackDTO): Promise { - return await this.usersRepository.connectOrcidForSignUp(callback) - } -} diff --git a/src/users/domain/useCases/GetOrcidSignUpAuthorizationUrl.ts b/src/users/domain/useCases/GetOrcidSignUpAuthorizationUrl.ts deleted file mode 100644 index 20953a9a..00000000 --- a/src/users/domain/useCases/GetOrcidSignUpAuthorizationUrl.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { UseCase } from '../../../core/domain/useCases/UseCase' -import { IUsersRepository } from '../repositories/IUsersRepository' - -export class GetOrcidSignUpAuthorizationUrl implements UseCase { - private usersRepository: IUsersRepository - - constructor(usersRepository: IUsersRepository) { - this.usersRepository = usersRepository - } - - /** - * Returns the ORCID authorization URL that sends a new user to ORCID to authenticate their ORCID iD while signing up. - * No authentication is used. - * - * @param {string} [redirectUri] - The URL ORCID redirects back to. Must be allowed by the installation's dataverse.api.signup.orcid-redirect-uris setting. - * @returns {Promise} - The ORCID authorization URL. - * @throws {ReadError} - If ORCID is not configured, or the redirect URI is not allowed. - */ - async execute(redirectUri: string): Promise { - return await this.usersRepository.getOrcidSignUpAuthorizationUrl(redirectUri) - } -} diff --git a/src/users/domain/useCases/SignUp.ts b/src/users/domain/useCases/SignUp.ts deleted file mode 100644 index 8f47c907..00000000 --- a/src/users/domain/useCases/SignUp.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { UseCase } from '../../../core/domain/useCases/UseCase' -import { SignUpDTO } from '../dtos/SignUpDTO' -import { SignUpResult } from '../models/SignUpResult' -import { IUsersRepository } from '../repositories/IUsersRepository' - -export class SignUp implements UseCase { - private usersRepository: IUsersRepository - - constructor(usersRepository: IUsersRepository) { - this.usersRepository = usersRepository - } - - /** - * Signs up a new builtin user account. No authentication is used, so it works for anonymous users. - * Requires the api-builtin-user-signup feature flag on the Dataverse installation. - * - * @param {SignUpDTO} [signUpDTO] - The new user's details and password. Include orcidToken (from connectOrcidForSignUp) to save an authenticated ORCID iD to the account. - * @returns {Promise} - status 'created' with the new user's identifier when the account was created, or status 'pending' when the installation requires email verification before creating it. - * @throws {WriteError} - If the sign-up fails. Its fieldErrors property maps invalid fields to their error messages. - */ - async execute(signUpDTO: SignUpDTO): Promise { - return await this.usersRepository.signUp(signUpDTO) - } -} diff --git a/src/users/index.ts b/src/users/index.ts index 5c83a9b0..e6d24914 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -4,9 +4,6 @@ import { RecreateCurrentApiToken } from './domain/useCases/RecreateCurrentApiTok import { GetCurrentApiToken } from './domain/useCases/GetCurrentApiToken' import { DeleteCurrentApiToken } from './domain/useCases/DeleteCurrentApiToken' import { RegisterUser } from './domain/useCases/RegisterUser' -import { SignUp } from './domain/useCases/SignUp' -import { GetOrcidSignUpAuthorizationUrl } from './domain/useCases/GetOrcidSignUpAuthorizationUrl' -import { ConnectOrcidForSignUp } from './domain/useCases/ConnectOrcidForSignUp' const usersRepository = new UsersRepository() @@ -15,28 +12,14 @@ const recreateCurrentApiToken = new RecreateCurrentApiToken(usersRepository) const getCurrentApiToken = new GetCurrentApiToken(usersRepository) const deleteCurrentApiToken = new DeleteCurrentApiToken(usersRepository) const registerUser = new RegisterUser(usersRepository) -const signUp = new SignUp(usersRepository) -const getOrcidSignUpAuthorizationUrl = new GetOrcidSignUpAuthorizationUrl(usersRepository) -const connectOrcidForSignUp = new ConnectOrcidForSignUp(usersRepository) export { getCurrentAuthenticatedUser, recreateCurrentApiToken, getCurrentApiToken, deleteCurrentApiToken, - registerUser, - signUp, - getOrcidSignUpAuthorizationUrl, - connectOrcidForSignUp + registerUser } export { AuthenticatedUser } from './domain/models/AuthenticatedUser' export { ApiTokenInfo } from './domain/models/ApiTokenInfo' export { UserDTO } from './domain/dtos/UserDTO' -export { SignUpDTO } from './domain/dtos/SignUpDTO' -export { OrcidSignUpCallbackDTO } from './domain/dtos/OrcidSignUpCallbackDTO' -export { - SignUpResult, - SignUpCreatedResult, - SignUpPendingResult -} from './domain/models/SignUpResult' -export { OrcidSignUpConnection } from './domain/models/OrcidSignUpConnection' diff --git a/src/users/infra/repositories/UsersRepository.ts b/src/users/infra/repositories/UsersRepository.ts index 43767306..476e713c 100644 --- a/src/users/infra/repositories/UsersRepository.ts +++ b/src/users/infra/repositories/UsersRepository.ts @@ -8,12 +8,6 @@ import { transformRecreateApiTokenResponseToApiTokenInfo } from './transformers/apiTokenInfoTransformers' import { UserDTO } from '../../domain/dtos/UserDTO' -import { SignUpDTO } from '../../domain/dtos/SignUpDTO' -import { OrcidSignUpCallbackDTO } from '../../domain/dtos/OrcidSignUpCallbackDTO' -import { SignUpResult } from '../../domain/models/SignUpResult' -import { OrcidSignUpConnection } from '../../domain/models/OrcidSignUpConnection' -import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' -import { transformOrcidSignUpConnectionResponseToOrcidSignUpConnection } from './transformers/orcidSignUpConnectionTransformers' export class UsersRepository extends ApiRepository implements IUsersRepository { private readonly usersResourceName: string = 'users' @@ -58,49 +52,6 @@ export class UsersRepository extends ApiRepository implements IUsersRepository { }) } - public async signUp(signUpDTO: SignUpDTO): Promise { - return this.doPost( - `/${this.usersResourceName}/signup`, - signUpDTO, - {}, - ApiConstants.CONTENT_TYPE_APPLICATION_JSON, - false - ) - .then((response): SignUpResult => { - if (response.status === 202) { - return { status: 'pending' } - } - return { status: 'created', identifier: response.data.data.identifier } - }) - .catch((error) => { - throw error - }) - } - - public async getOrcidSignUpAuthorizationUrl(redirectUri: string): Promise { - return this.doGet(`/${this.usersResourceName}/signup/orcid/authorize`, false, { redirectUri }) - .then((response) => response.data.data.authorizationUrl) - .catch((error) => { - throw error - }) - } - - public async connectOrcidForSignUp( - callback: OrcidSignUpCallbackDTO - ): Promise { - return this.doPost( - `/${this.usersResourceName}/signup/orcid`, - callback, - {}, - ApiConstants.CONTENT_TYPE_APPLICATION_JSON, - false - ) - .then((response) => transformOrcidSignUpConnectionResponseToOrcidSignUpConnection(response)) - .catch((error) => { - throw error - }) - } - private getAuthenticatedUserFromResponse(response: AxiosResponse): AuthenticatedUser { const responseData = response.data.data return { @@ -120,8 +71,7 @@ export class UsersRepository extends ApiRepository implements IUsersRepository { deactivatedTime: responseData.deactivatedTime, affiliation: responseData.affiliation, position: responseData.position, - emailLastConfirmed: responseData.emailLastConfirmed, - authenticatedOrcid: responseData.authenticatedOrcid + emailLastConfirmed: responseData.emailLastConfirmed } } } diff --git a/src/users/infra/repositories/transformers/orcidSignUpConnectionTransformers.ts b/src/users/infra/repositories/transformers/orcidSignUpConnectionTransformers.ts deleted file mode 100644 index a77546e4..00000000 --- a/src/users/infra/repositories/transformers/orcidSignUpConnectionTransformers.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AxiosResponse } from 'axios' -import { OrcidSignUpConnection } from '../../../domain/models/OrcidSignUpConnection' - -export const transformOrcidSignUpConnectionResponseToOrcidSignUpConnection = ( - response: AxiosResponse -): OrcidSignUpConnection => { - const data = response.data.data - return { - orcid: data.orcid, - orcidToken: data.orcidToken, - ...(data.firstName !== undefined && { firstName: data.firstName }), - ...(data.lastName !== undefined && { lastName: data.lastName }), - ...(data.emailAddress !== undefined && { emailAddress: data.emailAddress }), - ...(data.affiliation !== undefined && { affiliation: data.affiliation }), - ...(data.position !== undefined && { position: data.position }) - } -} diff --git a/test/environment/docker-compose.yml b/test/environment/docker-compose.yml index 1bccd93b..ad98d16b 100644 --- a/test/environment/docker-compose.yml +++ b/test/environment/docker-compose.yml @@ -11,8 +11,6 @@ services: DATAVERSE_DB_HOST: postgres DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} - DATAVERSE_FEATURE_API_BUILTIN_USER_SIGNUP: '1' - DATAVERSE_API_SIGNUP_RATE_LIMIT_PER_HOUR: '0' JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 -Ddataverse.pid.providers=fake -Ddataverse.pid.default-provider=fake diff --git a/test/integration/users/UsersRepository.test.ts b/test/integration/users/UsersRepository.test.ts index ada38767..e51f97c4 100644 --- a/test/integration/users/UsersRepository.test.ts +++ b/test/integration/users/UsersRepository.test.ts @@ -4,12 +4,8 @@ import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' import { TestConstants } from '../../testHelpers/TestConstants' -import { ReadError, SignUpDTO, WriteError } from '../../../src' +import { ReadError, WriteError } from '../../../src' import { createApiTokenViaApi } from '../../testHelpers/users/apiTokenHelper' -import { - deleteAuthenticatedUserViaAdminApi, - getAuthenticatedUserViaAdminApi -} from '../../testHelpers/users/builtinUserApiHelper' describe('UsersRepository', () => { const sut: UsersRepository = new UsersRepository() @@ -97,127 +93,4 @@ describe('UsersRepository', () => { await expect(sut.deleteCurrentApiToken()).rejects.toThrow(errorExpected) }) }) - - describe('signUp', () => { - const uniqueSuffix = Date.now().toString(36) - const testSignUpDTO: SignUpDTO = { - username: `signup${uniqueSuffix}`, - firstName: 'Ada', - lastName: 'Lovelace', - emailAddress: `signup${uniqueSuffix}@mailinator.com`, - password: 'Str0ngPassw0rd1', - affiliation: 'Harvard University', - position: 'Researcher', - termsAccepted: true - } - - beforeEach(() => { - ApiConfig.init( - TestConstants.TEST_API_URL, - DataverseApiAuthMechanism.API_KEY, - process.env.TEST_API_KEY - ) - }) - - afterAll(async () => { - await deleteAuthenticatedUserViaAdminApi(testSignUpDTO.username).catch(() => undefined) - }) - - test('should create a builtin user without sending the configured credentials', async () => { - ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, 'invalidApiKey') - - const actual = await sut.signUp(testSignUpDTO) - - expect(actual).toEqual({ status: 'created', identifier: `@${testSignUpDTO.username}` }) - const createdUser = await getAuthenticatedUserViaAdminApi(testSignUpDTO.username) - expect(createdUser.authenticationProviderId).toBe('builtin') - expect(createdUser.email).toBe(testSignUpDTO.emailAddress) - expect(createdUser.affiliation).toBe('Harvard University') - expect(createdUser.authenticatedOrcid).toBeUndefined() - }) - - test('should return field errors when the username and email are already taken', async () => { - let error: WriteError | undefined = undefined - await sut.signUp(testSignUpDTO).catch((e) => (error = e)) - - expect(error).toBeInstanceOf(WriteError) - expect((error as unknown as WriteError).statusCode).toBe(400) - expect((error as unknown as WriteError).fieldErrors).toEqual( - expect.objectContaining({ - username: 'This username is already taken.', - emailAddress: 'This email address is already taken.' - }) - ) - }) - - test('should return field errors for invalid values', async () => { - let error: WriteError | undefined = undefined - await sut - .signUp({ - username: 'not valid!', - firstName: '', - lastName: '', - emailAddress: 'not-an-email', - password: 'x', - termsAccepted: false - }) - .catch((e) => (error = e)) - - expect(error).toBeInstanceOf(WriteError) - expect((error as unknown as WriteError).statusCode).toBe(400) - expect(Object.keys((error as unknown as WriteError).fieldErrors ?? {})).toEqual( - expect.arrayContaining([ - 'username', - 'firstName', - 'lastName', - 'emailAddress', - 'password', - 'termsAccepted' - ]) - ) - }) - - test('should reject an invalid orcidToken with an orcid field error', async () => { - let error: WriteError | undefined = undefined - await sut - .signUp({ - ...testSignUpDTO, - username: `${testSignUpDTO.username}x`, - emailAddress: `x${testSignUpDTO.emailAddress}`, - orcidToken: 'not-a-valid-token' - }) - .catch((e) => (error = e)) - - expect(error).toBeInstanceOf(WriteError) - expect((error as unknown as WriteError).statusCode).toBe(400) - expect((error as unknown as WriteError).fieldErrors).toEqual({ - orcid: 'Your ORCID connection has expired. Please connect your ORCID iD again.' - }) - }) - }) - - describe('ORCID at sign-up without an ORCID provider', () => { - const redirectUri = 'http://localhost:8000/spa/sign-up' - - test('getOrcidSignUpAuthorizationUrl should return a 404 ReadError', async () => { - let error: ReadError | undefined = undefined - await sut.getOrcidSignUpAuthorizationUrl(redirectUri).catch((e) => (error = e)) - - expect(error).toBeInstanceOf(ReadError) - expect((error as unknown as ReadError).statusCode).toBe(404) - expect((error as unknown as ReadError).message).toContain( - 'ORCID is not configured on this installation.' - ) - }) - - test('connectOrcidForSignUp should return a 404 WriteError', async () => { - let error: WriteError | undefined = undefined - await sut - .connectOrcidForSignUp({ code: 'code', state: 'state', redirectUri }) - .catch((e) => (error = e)) - - expect(error).toBeInstanceOf(WriteError) - expect((error as unknown as WriteError).statusCode).toBe(404) - }) - }) }) diff --git a/test/testHelpers/users/builtinUserApiHelper.ts b/test/testHelpers/users/builtinUserApiHelper.ts index 957705a0..d91e08a3 100644 --- a/test/testHelpers/users/builtinUserApiHelper.ts +++ b/test/testHelpers/users/builtinUserApiHelper.ts @@ -25,15 +25,3 @@ export const createBuiltInUser = async (userName: string): Promise => { throw new Error(`Error while creating API token`) } } - -export const getAuthenticatedUserViaAdminApi = async ( - userIdentifier: string -): Promise> => { - return axios - .get(`${TestConstants.TEST_API_URL}/admin/authenticatedUsers/${userIdentifier}`) - .then((response) => response.data.data) -} - -export const deleteAuthenticatedUserViaAdminApi = async (userIdentifier: string): Promise => { - await axios.delete(`${TestConstants.TEST_API_URL}/admin/authenticatedUsers/${userIdentifier}`) -} diff --git a/test/unit/core/RepositoryError.test.ts b/test/unit/core/RepositoryError.test.ts deleted file mode 100644 index 6ab6e1b0..00000000 --- a/test/unit/core/RepositoryError.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ReadError, WriteError } from '../../../src' - -describe('RepositoryError details', () => { - test('keeps the existing message and exposes statusCode and fieldErrors', () => { - const error = new WriteError('[400] Invalid fields.', { - statusCode: 400, - fieldErrors: { username: 'This username is already taken.' } - }) - - expect(error.message).toBe( - 'There was an error when writing the resource. Reason was: [400] Invalid fields.' - ) - expect(error.statusCode).toBe(400) - expect(error.fieldErrors).toEqual({ username: 'This username is already taken.' }) - }) - - test('leaves the details undefined when none are given', () => { - const error = new ReadError('[404] Not found.') - - expect(error.message).toBe( - 'There was an error when reading the resource. Reason was: [404] Not found.' - ) - expect(error.statusCode).toBeUndefined() - expect(error.fieldErrors).toBeUndefined() - }) -}) diff --git a/test/unit/users/ConnectOrcidForSignUp.test.ts b/test/unit/users/ConnectOrcidForSignUp.test.ts deleted file mode 100644 index 0c2595ee..00000000 --- a/test/unit/users/ConnectOrcidForSignUp.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ConnectOrcidForSignUp } from '../../../src/users/domain/useCases/ConnectOrcidForSignUp' -import { IUsersRepository } from '../../../src/users/domain/repositories/IUsersRepository' -import { OrcidSignUpCallbackDTO, OrcidSignUpConnection, WriteError } from '../../../src' - -describe('execute', () => { - const callback: OrcidSignUpCallbackDTO = { - code: 'orcid-code', - state: 'orcid-state', - redirectUri: 'http://127.0.0.1:8000/spa/sign-up' - } - - test('should return the ORCID connection on repository success', async () => { - const connection: OrcidSignUpConnection = { - orcid: 'https://orcid.org/0000-0002-1825-0097', - orcidToken: 'orcid-token', - firstName: 'Josiah' - } - const usersRepositoryStub: IUsersRepository = {} as IUsersRepository - usersRepositoryStub.connectOrcidForSignUp = jest.fn().mockResolvedValue(connection) - const sut = new ConnectOrcidForSignUp(usersRepositoryStub) - - const actual = await sut.execute(callback) - - expect(usersRepositoryStub.connectOrcidForSignUp).toHaveBeenCalledWith(callback) - expect(actual).toEqual(connection) - }) - - test('should return error result on repository error', async () => { - const usersRepositoryStub: IUsersRepository = {} as IUsersRepository - usersRepositoryStub.connectOrcidForSignUp = jest.fn().mockRejectedValue(new WriteError()) - const sut = new ConnectOrcidForSignUp(usersRepositoryStub) - - await expect(sut.execute(callback)).rejects.toThrow(WriteError) - }) -}) diff --git a/test/unit/users/GetOrcidSignUpAuthorizationUrl.test.ts b/test/unit/users/GetOrcidSignUpAuthorizationUrl.test.ts deleted file mode 100644 index 1991bfdb..00000000 --- a/test/unit/users/GetOrcidSignUpAuthorizationUrl.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { GetOrcidSignUpAuthorizationUrl } from '../../../src/users/domain/useCases/GetOrcidSignUpAuthorizationUrl' -import { IUsersRepository } from '../../../src/users/domain/repositories/IUsersRepository' -import { ReadError } from '../../../src' - -describe('execute', () => { - const redirectUri = 'http://127.0.0.1:8000/spa/sign-up' - - test('should return the authorization URL on repository success', async () => { - const usersRepositoryStub: IUsersRepository = {} as IUsersRepository - usersRepositoryStub.getOrcidSignUpAuthorizationUrl = jest - .fn() - .mockResolvedValue('https://orcid.org/oauth/authorize?client_id=APP-1') - const sut = new GetOrcidSignUpAuthorizationUrl(usersRepositoryStub) - - const actual = await sut.execute(redirectUri) - - expect(usersRepositoryStub.getOrcidSignUpAuthorizationUrl).toHaveBeenCalledWith(redirectUri) - expect(actual).toBe('https://orcid.org/oauth/authorize?client_id=APP-1') - }) - - test('should return error result on repository error', async () => { - const usersRepositoryStub: IUsersRepository = {} as IUsersRepository - usersRepositoryStub.getOrcidSignUpAuthorizationUrl = jest - .fn() - .mockRejectedValue(new ReadError()) - const sut = new GetOrcidSignUpAuthorizationUrl(usersRepositoryStub) - - await expect(sut.execute(redirectUri)).rejects.toThrow(ReadError) - }) -}) diff --git a/test/unit/users/SignUp.test.ts b/test/unit/users/SignUp.test.ts deleted file mode 100644 index 3e0effca..00000000 --- a/test/unit/users/SignUp.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SignUp } from '../../../src/users/domain/useCases/SignUp' -import { IUsersRepository } from '../../../src/users/domain/repositories/IUsersRepository' -import { SignUpDTO, WriteError } from '../../../src' - -describe('execute', () => { - const testSignUpDTO: SignUpDTO = { - username: 'alovelace', - firstName: 'Ada', - lastName: 'Lovelace', - emailAddress: 'ada@example.edu', - password: 'Str0ngPassw0rd', - termsAccepted: true - } - - test('should return the sign-up result on repository success', async () => { - const usersRepositoryStub: IUsersRepository = {} as IUsersRepository - usersRepositoryStub.signUp = jest - .fn() - .mockResolvedValue({ status: 'created', identifier: '@alovelace' }) - const sut = new SignUp(usersRepositoryStub) - - const actual = await sut.execute(testSignUpDTO) - - expect(usersRepositoryStub.signUp).toHaveBeenCalledWith(testSignUpDTO) - expect(actual).toEqual({ status: 'created', identifier: '@alovelace' }) - }) - - test('should return error result on repository error', async () => { - const usersRepositoryStub: IUsersRepository = {} as IUsersRepository - usersRepositoryStub.signUp = jest.fn().mockRejectedValue(new WriteError()) - const sut = new SignUp(usersRepositoryStub) - - await expect(sut.execute(testSignUpDTO)).rejects.toThrow(WriteError) - }) -}) diff --git a/test/unit/users/UsersRepository.test.ts b/test/unit/users/UsersRepository.test.ts index 68e39c33..f7117a5c 100644 --- a/test/unit/users/UsersRepository.test.ts +++ b/test/unit/users/UsersRepository.test.ts @@ -6,7 +6,7 @@ import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' import { TestConstants } from '../../testHelpers/TestConstants' -import { ReadError, SignUpDTO, WriteError } from '../../../src' +import { ReadError } from '../../../src' describe('getCurrentAuthenticatedUser', () => { const sut: UsersRepository = new UsersRepository() @@ -77,215 +77,3 @@ describe('getCurrentAuthenticatedUser', () => { expect(error).toBeInstanceOf(Error) }) }) - -describe('getCurrentAuthenticatedUser authenticatedOrcid', () => { - const sut: UsersRepository = new UsersRepository() - - beforeEach(() => { - ApiConfig.init( - TestConstants.TEST_API_URL, - DataverseApiAuthMechanism.API_KEY, - TestConstants.TEST_DUMMY_API_KEY - ) - }) - - test('should map authenticatedOrcid when present', async () => { - const testAuthenticatedUser = createAuthenticatedUser() - jest.spyOn(axios, 'get').mockResolvedValue({ - data: { - status: 'OK', - data: { - ...testAuthenticatedUser, - authenticatedOrcid: 'https://orcid.org/0000-0002-1825-0097' - } - } - }) - - const actual = await sut.getCurrentAuthenticatedUser() - - expect(actual.authenticatedOrcid).toBe('https://orcid.org/0000-0002-1825-0097') - }) -}) - -describe('signUp', () => { - const sut: UsersRepository = new UsersRepository() - const testSignUpDTO: SignUpDTO = { - username: 'alovelace', - firstName: 'Ada', - lastName: 'Lovelace', - emailAddress: 'ada@example.edu', - password: 'Str0ngPassw0rd', - affiliation: 'Harvard University', - termsAccepted: true, - orcidToken: 'orcid-token' - } - const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/users/signup` - - beforeEach(() => { - ApiConfig.init( - TestConstants.TEST_API_URL, - DataverseApiAuthMechanism.API_KEY, - TestConstants.TEST_DUMMY_API_KEY - ) - }) - - test('should post the sign-up without credentials and return created', async () => { - jest.spyOn(axios, 'post').mockResolvedValue({ - status: 201, - data: { status: 'OK', data: { identifier: '@alovelace' } } - }) - - const actual = await sut.signUp(testSignUpDTO) - - expect(axios.post).toHaveBeenCalledWith( - expectedApiEndpoint, - JSON.stringify(testSignUpDTO), - TestConstants.TEST_EXPECTED_UNAUTHENTICATED_REQUEST_CONFIG - ) - expect(actual).toEqual({ status: 'created', identifier: '@alovelace' }) - }) - - test('should return pending when the response is 202 Accepted', async () => { - jest.spyOn(axios, 'post').mockResolvedValue({ - status: 202, - data: { status: 'OK', data: { message: 'Check your email.' } } - }) - - const actual = await sut.signUp(testSignUpDTO) - - expect(actual).toEqual({ status: 'pending' }) - }) - - test('should reject with a WriteError carrying statusCode and fieldErrors', async () => { - jest.spyOn(axios, 'post').mockRejectedValue({ - response: { - status: 400, - data: { - status: 'ERROR', - message: 'The provided fields are invalid for signing up a new user.', - fieldErrors: { username: 'This username is already taken.' } - } - } - }) - - let error: WriteError | undefined = undefined - await sut.signUp(testSignUpDTO).catch((e) => (error = e)) - - expect(error).toBeInstanceOf(WriteError) - expect((error as unknown as WriteError).statusCode).toBe(400) - expect((error as unknown as WriteError).fieldErrors).toEqual({ - username: 'This username is already taken.' - }) - expect((error as unknown as WriteError).message).toContain( - '[400] The provided fields are invalid for signing up a new user.' - ) - }) -}) - -describe('getOrcidSignUpAuthorizationUrl', () => { - const sut: UsersRepository = new UsersRepository() - const redirectUri = 'http://127.0.0.1:8000/spa/sign-up' - - beforeEach(() => { - ApiConfig.init( - TestConstants.TEST_API_URL, - DataverseApiAuthMechanism.API_KEY, - TestConstants.TEST_DUMMY_API_KEY - ) - }) - - test('should request the authorization URL without credentials', async () => { - jest.spyOn(axios, 'get').mockResolvedValue({ - status: 200, - data: { - status: 'OK', - data: { authorizationUrl: 'https://orcid.org/oauth/authorize?client_id=APP-1' } - } - }) - - const actual = await sut.getOrcidSignUpAuthorizationUrl(redirectUri) - - expect(axios.get).toHaveBeenCalledWith( - `${TestConstants.TEST_API_URL}/users/signup/orcid/authorize`, - { ...TestConstants.TEST_EXPECTED_UNAUTHENTICATED_REQUEST_CONFIG, params: { redirectUri } } - ) - expect(actual).toBe('https://orcid.org/oauth/authorize?client_id=APP-1') - }) - - test('should reject with a ReadError carrying the status code', async () => { - jest.spyOn(axios, 'get').mockRejectedValue({ - response: { - status: 404, - data: { status: 'ERROR', message: 'ORCID is not configured on this installation.' } - } - }) - - let error: ReadError | undefined = undefined - await sut.getOrcidSignUpAuthorizationUrl(redirectUri).catch((e) => (error = e)) - - expect(error).toBeInstanceOf(ReadError) - expect((error as unknown as ReadError).statusCode).toBe(404) - expect((error as unknown as ReadError).fieldErrors).toBeUndefined() - }) -}) - -describe('connectOrcidForSignUp', () => { - const sut: UsersRepository = new UsersRepository() - const callback = { - code: 'orcid-code', - state: 'orcid-state', - redirectUri: 'http://127.0.0.1:8000/spa/sign-up' - } - - beforeEach(() => { - ApiConfig.init( - TestConstants.TEST_API_URL, - DataverseApiAuthMechanism.API_KEY, - TestConstants.TEST_DUMMY_API_KEY - ) - }) - - test('should post the ORCID callback without credentials and map the connection', async () => { - jest.spyOn(axios, 'post').mockResolvedValue({ - status: 200, - data: { - status: 'OK', - data: { - orcid: 'https://orcid.org/0000-0002-1825-0097', - orcidToken: 'orcid-token', - firstName: 'Josiah', - lastName: 'Carberry' - } - } - }) - - const actual = await sut.connectOrcidForSignUp(callback) - - expect(axios.post).toHaveBeenCalledWith( - `${TestConstants.TEST_API_URL}/users/signup/orcid`, - JSON.stringify(callback), - TestConstants.TEST_EXPECTED_UNAUTHENTICATED_REQUEST_CONFIG - ) - expect(actual).toEqual({ - orcid: 'https://orcid.org/0000-0002-1825-0097', - orcidToken: 'orcid-token', - firstName: 'Josiah', - lastName: 'Carberry' - }) - }) - - test('should reject with a 409 WriteError when the ORCID iD is already linked', async () => { - jest.spyOn(axios, 'post').mockRejectedValue({ - response: { - status: 409, - data: { status: 'ERROR', message: 'This ORCID iD is already linked to another account.' } - } - }) - - let error: WriteError | undefined = undefined - await sut.connectOrcidForSignUp(callback).catch((e) => (error = e)) - - expect(error).toBeInstanceOf(WriteError) - expect((error as unknown as WriteError).statusCode).toBe(409) - }) -})