Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 0 additions & 106 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/core/domain/repositories/ReadError.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
12 changes: 1 addition & 11 deletions src/core/domain/repositories/RepositoryError.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
export interface RepositoryErrorDetails {
statusCode?: number
fieldErrors?: Record<string, string>
}

export abstract class RepositoryError extends Error {
readonly statusCode?: number
readonly fieldErrors?: Record<string, string>

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
}
}
6 changes: 3 additions & 3 deletions src/core/domain/repositories/WriteError.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 0 additions & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
26 changes: 7 additions & 19 deletions src/core/infra/repositories/ApiRepository.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -15,18 +14,17 @@ 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))
})
}

public async doPost(
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<AxiosResponse> {
return await this.doRequest('post', apiEndpoint, data, queryParams, contentType, authRequired)
return await this.doRequest('post', apiEndpoint, data, queryParams, contentType)
}

public async doPut(
Expand All @@ -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))
})
}

Expand All @@ -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 =
Expand All @@ -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<AxiosResponse> {
let requestData = data

Expand All @@ -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))
}
}
}
5 changes: 0 additions & 5 deletions src/users/domain/dtos/OrcidSignUpCallbackDTO.ts

This file was deleted.

11 changes: 0 additions & 11 deletions src/users/domain/dtos/SignUpDTO.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/users/domain/models/AuthenticatedUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@ export interface AuthenticatedUser {
affiliation?: string
position?: string
emailLastConfirmed?: string
authenticatedOrcid?: string
}
9 changes: 0 additions & 9 deletions src/users/domain/models/OrcidSignUpConnection.ts

This file was deleted.

10 changes: 0 additions & 10 deletions src/users/domain/models/SignUpResult.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/users/domain/repositories/IUsersRepository.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
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<AuthenticatedUser>
recreateCurrentApiToken(): Promise<ApiTokenInfo>
getCurrentApiToken(): Promise<ApiTokenInfo>
deleteCurrentApiToken(): Promise<void>
registerUser(userDTO: UserDTO): Promise<void>
signUp(signUpDTO: SignUpDTO): Promise<SignUpResult>
getOrcidSignUpAuthorizationUrl(redirectUri: string): Promise<string>
connectOrcidForSignUp(callback: OrcidSignUpCallbackDTO): Promise<OrcidSignUpConnection>
}
24 changes: 0 additions & 24 deletions src/users/domain/useCases/ConnectOrcidForSignUp.ts

This file was deleted.

22 changes: 0 additions & 22 deletions src/users/domain/useCases/GetOrcidSignUpAuthorizationUrl.ts

This file was deleted.

Loading
Loading