Skip to content

Commit

Permalink
Merge pull request #193 from IQSS/190-api-tokens
Browse files Browse the repository at this point in the history
Api token management use cases
  • Loading branch information
ChengShi-1 authored Sep 27, 2024
2 parents e246194 + 8ac7738 commit b994225
Show file tree
Hide file tree
Showing 20 changed files with 487 additions and 27 deletions.
64 changes: 64 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ The different use cases currently available in the package are classified below,
- [Users](#Users)
- [Users read use cases](#users-read-use-cases)
- [Get Current Authenticated User](#get-current-authenticated-user)
- [Get Current API Token](#get-current-api-token)
- [Users write use cases](#users-write-use-cases)
- [Delete Current API Token](#delete-current-api-token)
- [Recreate Current API Token](#recreate-current-api-token)
- [Info](#Info)
- [Get Dataverse Backend Version](#get-dataverse-backend-version)
- [Get Maximum Embargo Duration In Months](#get-maximum-embargo-duration-in-months)
Expand Down Expand Up @@ -1172,6 +1176,66 @@ getCurrentAuthenticatedUser.execute().then((user: AuthenticatedUser) => {

_See [use case](../src/users/domain/useCases/GetCurrentAuthenticatedUser.ts) implementation_.

### Get Current API Token

Returns the current [ApiTokenInfo](../src/users/domain/models/ApiTokenInfo.ts) corresponding to the current authenticated user.

##### Example call:

```typescript
import { getCurrentApiToken } from '@iqss/dataverse-client-javascript'

/* ... */

getCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfo) => {
/* ... */
})

/* ... */
```

_See [use case](../src/users/domain/useCases/GetCurrentApiToken.ts) implementation_.

### Users write use cases

### Delete Current API Token

Deletes the API token of the current authenticated user.

##### Example call:

```typescript
import { deleteCurrentApiToken } from '@iqss/dataverse-client-javascript'

/* ... */

deleteCurrentApiToken.execute()

/* ... */
```

_See [use case](../src/users/domain/useCases/DeleteCurrentApiToken.ts) implementation_.

### Recreate Current API Token

Reacreates the API token of the current authenticated user and returns the new [ApiTokenInfo](../src/users/domain/models/ApiTokenInfo.ts).

##### Example call:

```typescript
import { recreateCurrentApiToken } from '@iqss/dataverse-client-javascript'

/* ... */

recreateCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfo) => {
/* ... */
})

/* ... */
```

_See [use case](../src/users/domain/useCases/RecreateCurrentApiToken.ts) implementation_.

## Info

#### Get Dataverse Backend Version
Expand Down
9 changes: 9 additions & 0 deletions src/core/infra/repositories/ApiRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ export abstract class ApiRepository {
return await this.doRequest('put', apiEndpoint, data, queryParams)
}

public async doDelete(apiEndpoint: string, queryParams: object = {}): Promise<AxiosResponse> {
return await axios
.delete(buildRequestUrl(apiEndpoint), buildRequestConfig(true, queryParams))
.then((response) => response)
.catch((error) => {
throw new WriteError(this.buildErrorMessage(error))
})
}

protected buildApiEndpoint(
resourceName: string,
operation: string,
Expand Down
4 changes: 4 additions & 0 deletions src/users/domain/models/ApiTokenInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ApiTokenInfo {
apiToken: string
expirationDate: Date
}
4 changes: 4 additions & 0 deletions src/users/domain/repositories/IUsersRepository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ApiTokenInfo } from '../models/ApiTokenInfo'
import { AuthenticatedUser } from '../models/AuthenticatedUser'

export interface IUsersRepository {
getCurrentAuthenticatedUser(): Promise<AuthenticatedUser>
recreateCurrentApiToken(): Promise<ApiTokenInfo>
getCurrentApiToken(): Promise<ApiTokenInfo>
deleteCurrentApiToken(): Promise<void>
}
19 changes: 19 additions & 0 deletions src/users/domain/useCases/DeleteCurrentApiToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { IUsersRepository } from '../repositories/IUsersRepository'

export class DeleteCurrentApiToken implements UseCase<void> {
private usersRepository: IUsersRepository

constructor(usersRepository: IUsersRepository) {
this.usersRepository = usersRepository
}

/**
* Deletes the API token of the current authenticated user.
*
* @returns {Promise<void>}
*/
async execute(): Promise<void> {
return await this.usersRepository.deleteCurrentApiToken()
}
}
20 changes: 20 additions & 0 deletions src/users/domain/useCases/GetCurrentApiToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { ApiTokenInfo } from '../models/ApiTokenInfo'
import { IUsersRepository } from '../repositories/IUsersRepository'

export class GetCurrentApiToken implements UseCase<ApiTokenInfo> {
private usersRepository: IUsersRepository

constructor(usersRepository: IUsersRepository) {
this.usersRepository = usersRepository
}

/**
* Returns the current ApiTokenInfo corresponding to the current authenticated user.
*
* @returns {Promise<ApiTokenInfo>}
*/
async execute(): Promise<ApiTokenInfo> {
return await this.usersRepository.getCurrentApiToken()
}
}
20 changes: 20 additions & 0 deletions src/users/domain/useCases/RecreateCurrentApiToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { ApiTokenInfo } from '../models/ApiTokenInfo'
import { IUsersRepository } from '../repositories/IUsersRepository'

export class RecreateCurrentApiToken implements UseCase<ApiTokenInfo> {
private usersRepository: IUsersRepository

constructor(usersRepository: IUsersRepository) {
this.usersRepository = usersRepository
}

/**
* Reacreates the API token of the current authenticated user and returns the new ApiTokenInfo.
*
* @returns {Promise<ApiTokenInfo>}
*/
async execute(): Promise<ApiTokenInfo> {
return await this.usersRepository.recreateCurrentApiToken()
}
}
18 changes: 16 additions & 2 deletions src/users/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { UsersRepository } from './infra/repositories/UsersRepository'
import { GetCurrentAuthenticatedUser } from './domain/useCases/GetCurrentAuthenticatedUser'
import { RecreateCurrentApiToken } from './domain/useCases/RecreateCurrentApiToken'
import { GetCurrentApiToken } from './domain/useCases/GetCurrentApiToken'
import { DeleteCurrentApiToken } from './domain/useCases/DeleteCurrentApiToken'

const getCurrentAuthenticatedUser = new GetCurrentAuthenticatedUser(new UsersRepository())
const usersRepository = new UsersRepository()

export { getCurrentAuthenticatedUser }
const getCurrentAuthenticatedUser = new GetCurrentAuthenticatedUser(usersRepository)
const recreateCurrentApiToken = new RecreateCurrentApiToken(usersRepository)
const getCurrentApiToken = new GetCurrentApiToken(usersRepository)
const deleteCurrentApiToken = new DeleteCurrentApiToken(usersRepository)

export {
getCurrentAuthenticatedUser,
recreateCurrentApiToken,
getCurrentApiToken,
deleteCurrentApiToken
}
export { AuthenticatedUser } from './domain/models/AuthenticatedUser'
export { ApiTokenInfo } from './domain/models/ApiTokenInfo'
33 changes: 32 additions & 1 deletion src/users/infra/repositories/UsersRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,47 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'
import { IUsersRepository } from '../../domain/repositories/IUsersRepository'
import { AuthenticatedUser } from '../../domain/models/AuthenticatedUser'
import { AxiosResponse } from 'axios'
import { ApiTokenInfo } from '../../domain/models/ApiTokenInfo'
import {
transformGetApiTokenResponseToApiTokenInfo,
transformRecreateApiTokenResponseToApiTokenInfo
} from './transformers/apiTokenInfoTransformers'

export class UsersRepository extends ApiRepository implements IUsersRepository {
private readonly usersResourceName: string = 'users'

public async getCurrentAuthenticatedUser(): Promise<AuthenticatedUser> {
return this.doGet('/users/:me', true)
return this.doGet(`/${this.usersResourceName}/:me`, true)
.then((response) => this.getAuthenticatedUserFromResponse(response))
.catch((error) => {
throw error
})
}

public async recreateCurrentApiToken(): Promise<ApiTokenInfo> {
return this.doPost(`/${this.usersResourceName}/token/recreate?returnExpiration=true`, {})
.then((response) => transformRecreateApiTokenResponseToApiTokenInfo(response))
.catch((error) => {
throw error
})
}

public async getCurrentApiToken(): Promise<ApiTokenInfo> {
return this.doGet(`/${this.usersResourceName}/token`, true)
.then((response) => transformGetApiTokenResponseToApiTokenInfo(response))
.catch((error) => {
throw error
})
}

public async deleteCurrentApiToken(): Promise<void> {
return this.doDelete(`/${this.usersResourceName}/token`)
.then(() => undefined)
.catch((error) => {
throw error
})
}

private getAuthenticatedUserFromResponse(response: AxiosResponse): AuthenticatedUser {
const responseData = response.data.data
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AxiosResponse } from 'axios'
import { ApiTokenInfo } from '../../../domain/models/ApiTokenInfo'

export const transformGetApiTokenResponseToApiTokenInfo = (
response: AxiosResponse
): ApiTokenInfo => {
const apiTokenInfoPayload = response.data.data
const messageParts = apiTokenInfoPayload.message.split(' ')
const expirationDateFormattedTimestamp = `${messageParts[4]}T${messageParts[5]}`
return {
apiToken: messageParts[1],
expirationDate: new Date(expirationDateFormattedTimestamp)
}
}

export const transformRecreateApiTokenResponseToApiTokenInfo = (
response: AxiosResponse
): ApiTokenInfo => {
const apiTokenInfoPayload = response.data.data
const messageParts = apiTokenInfoPayload.message.split(' ')
const expirationDateFormattedTimestamp = `${messageParts[9]}T${messageParts[10]}`
return {
apiToken: messageParts[5],
expirationDate: new Date(expirationDateFormattedTimestamp)
}
}
4 changes: 2 additions & 2 deletions test/environment/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
POSTGRES_VERSION=13
DATAVERSE_DB_USER=dataverse
SOLR_VERSION=9.3.0
DATAVERSE_IMAGE_REGISTRY=docker.io
DATAVERSE_IMAGE_TAG=unstable
DATAVERSE_IMAGE_REGISTRY=ghcr.io
DATAVERSE_IMAGE_TAG=10857-add-expiration-date-to-recreate-token-api
DATAVERSE_BOOTSTRAP_TIMEOUT=5m
30 changes: 30 additions & 0 deletions test/functional/users/DeleteCurrentApiToken.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ApiConfig, WriteError, deleteCurrentApiToken } from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { TestConstants } from '../../testHelpers/TestConstants'
import { createApiTokenViaApi } from '../../testHelpers/users/apiTokenHelper'

describe('execute', () => {
beforeAll(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
})

afterAll(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
})

test('should successfully recreate the API token', async () => {
const testApiToken = await createApiTokenViaApi('deleteCurrentApiTokenFTUser')
ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, testApiToken)
await deleteCurrentApiToken.execute()
// Since the token has been deleted, the next call using it should return a WriteError
await expect(deleteCurrentApiToken.execute()).rejects.toBeInstanceOf(WriteError)
})
})
19 changes: 19 additions & 0 deletions test/functional/users/GetCurrentApiToken.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ApiConfig, getCurrentApiToken } from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { TestConstants } from '../../testHelpers/TestConstants'

describe('execute', () => {
beforeAll(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
})

test('should return the current API token', async () => {
const actualTokenInfo = await getCurrentApiToken.execute()
expect(actualTokenInfo.apiToken).toBe(process.env.TEST_API_KEY)
expect(typeof actualTokenInfo.expirationDate).toBe('object')
})
})
31 changes: 31 additions & 0 deletions test/functional/users/RecreateCurrentApiToken.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ApiConfig, recreateCurrentApiToken } from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { TestConstants } from '../../testHelpers/TestConstants'
import { createApiTokenViaApi } from '../../testHelpers/users/apiTokenHelper'

describe('execute', () => {
beforeAll(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
})

afterAll(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
})

test('should successfully recreate the API token', async () => {
const testApiToken = await createApiTokenViaApi('recreateCurrentApiTokenFTUser')
ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, testApiToken)
const actualRecreatedApiTokenInfo = await recreateCurrentApiToken.execute()
expect(actualRecreatedApiTokenInfo.apiToken).not.toBeUndefined()
expect(actualRecreatedApiTokenInfo.apiToken).not.toBe(testApiToken)
expect(typeof actualRecreatedApiTokenInfo.expirationDate).toBe('object')
})
})
Loading

0 comments on commit b994225

Please sign in to comment.