Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api token management use cases #193

Merged
merged 8 commits into from
Sep 27, 2024
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
64 changes: 64 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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)
g-saracca marked this conversation as resolved.
Show resolved Hide resolved
- [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 @@ -1137,6 +1141,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]}`
g-saracca marked this conversation as resolved.
Show resolved Hide resolved
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
Loading