-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #57 from IQSS/53-users-me
56 - Add use cases to get the current authenticated user and log out
- Loading branch information
Showing
28 changed files
with
440 additions
and
3,087 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface IAuthRepository { | ||
logout(): Promise<void>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { UseCase } from '../../../core/domain/useCases/UseCase'; | ||
import { IAuthRepository } from '../repositories/IAuthRepository'; | ||
|
||
export class Logout implements UseCase<void> { | ||
private authRepository: IAuthRepository; | ||
|
||
constructor(logoutRepository: IAuthRepository) { | ||
this.authRepository = logoutRepository; | ||
} | ||
|
||
async execute(): Promise<void> { | ||
await this.authRepository.logout(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { AuthRepository } from './infra/repositories/AuthRepository'; | ||
import { Logout } from './domain/useCases/Logout'; | ||
|
||
const logout = new Logout(new AuthRepository()); | ||
|
||
export { logout }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'; | ||
import { IAuthRepository } from '../../domain/repositories/IAuthRepository'; | ||
|
||
export class AuthRepository extends ApiRepository implements IAuthRepository { | ||
public async logout(): Promise<void> { | ||
return this.doPost('/logout', '') | ||
.then(() => undefined) | ||
.catch((error) => { | ||
throw error; | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,7 @@ | ||
export class ReadError extends Error { | ||
import { RepositoryError } from './RepositoryError'; | ||
|
||
export class ReadError extends RepositoryError { | ||
constructor(reason?: string) { | ||
let message = 'There was an error when reading the resource.'; | ||
if (reason) { | ||
message += ` Reason was: ${reason}`; | ||
} | ||
super(message); | ||
super('There was an error when reading the resource.', reason); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export abstract class RepositoryError extends Error { | ||
constructor(message: string, reason?: string) { | ||
if (reason) { | ||
message += ` Reason was: ${reason}`; | ||
} | ||
super(message); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { RepositoryError } from './RepositoryError'; | ||
|
||
export class WriteError extends RepositoryError { | ||
constructor(reason?: string) { | ||
super('There was an error when writing the resource.', reason); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
export { ReadError } from './domain/repositories/ReadError'; | ||
export { WriteError } from './domain/repositories/WriteError'; | ||
export { ApiConfig } from './infra/repositories/ApiConfig'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export class ApiConfig { | ||
static DATAVERSE_API_URL: string; | ||
|
||
static init(dataverseApiUrl: string) { | ||
this.DATAVERSE_API_URL = dataverseApiUrl; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import axios, { AxiosResponse } from 'axios'; | ||
import { ApiConfig } from './ApiConfig'; | ||
import { ReadError } from '../../domain/repositories/ReadError'; | ||
import { WriteError } from '../../domain/repositories/WriteError'; | ||
|
||
/* TODO: | ||
We set { withCredentials: true } to send the JSESSIONID cookie in the requests for API authentication. | ||
This is required, along with the session auth feature flag enabled in the backend, to be able to authenticate using the JSESSIONID cookie. | ||
Auth mechanisms like this must be configurable to set the one that fits the particular use case of js-dataverse. (For the SPA MVP, it is the session cookie API auth). | ||
For 2.0.0, we must also support API key auth to be backwards compatible and support use cases other than SPA MVP. | ||
*/ | ||
export abstract class ApiRepository { | ||
public async doGet(apiEndpoint: string, withCredentials: boolean = false): Promise<AxiosResponse> { | ||
return await axios | ||
.get(this.buildRequestUrl(apiEndpoint), { withCredentials: withCredentials }) | ||
.then((response) => response) | ||
.catch((error) => { | ||
throw new ReadError( | ||
`[${error.response.status}]${error.response.data ? ` ${error.response.data.message}` : ''}`, | ||
); | ||
}); | ||
} | ||
|
||
public async doPost(apiEndpoint: string, data: string | object): Promise<AxiosResponse> { | ||
return await axios | ||
.post(this.buildRequestUrl(apiEndpoint), JSON.stringify(data), { | ||
withCredentials: true, | ||
headers: { 'Content-Type': 'application/json' }, | ||
}) | ||
.then((response) => response) | ||
.catch((error) => { | ||
throw new WriteError( | ||
`[${error.response.status}]${error.response.data ? ` ${error.response.data.message}` : ''}`, | ||
); | ||
}); | ||
} | ||
|
||
private buildRequestUrl(apiEndpoint: string): string { | ||
return `${ApiConfig.DATAVERSE_API_URL}${apiEndpoint}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
export * from './core'; | ||
export * from './info'; | ||
export * from './users'; | ||
export * from './auth'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import { DataverseInfoRepository } from './infra/repositories/DataverseInfoRepository'; | ||
import { GetDataverseVersion } from './domain/useCases/GetDataverseVersion'; | ||
|
||
const getDataverseVersion = new GetDataverseVersion(new DataverseInfoRepository(process.env.DATAVERSE_API_URL)); | ||
const getDataverseVersion = new GetDataverseVersion(new DataverseInfoRepository()); | ||
|
||
export { getDataverseVersion }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export interface AuthenticatedUser { | ||
id: number; | ||
persistentUserId: string; | ||
identifier: string; | ||
displayName: string; | ||
firstName: string; | ||
lastName: string; | ||
email: string; | ||
superuser: boolean; | ||
deactivated: boolean; | ||
createdTime: string; | ||
authenticationProviderId: string; | ||
lastLoginTime?: string; | ||
lastApiUseTime?: string; | ||
deactivatedTime?: string; | ||
affiliation?: string; | ||
position?: string; | ||
emailLastConfirmed?: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { AuthenticatedUser } from '../models/AuthenticatedUser'; | ||
|
||
export interface IUsersRepository { | ||
getCurrentAuthenticatedUser(): Promise<AuthenticatedUser>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { UseCase } from '../../../core/domain/useCases/UseCase'; | ||
import { IUsersRepository } from '../repositories/IUsersRepository'; | ||
import { AuthenticatedUser } from '../models/AuthenticatedUser'; | ||
|
||
export class GetCurrentAuthenticatedUser implements UseCase<AuthenticatedUser> { | ||
private usersRepository: IUsersRepository; | ||
|
||
constructor(usersRepository: IUsersRepository) { | ||
this.usersRepository = usersRepository; | ||
} | ||
|
||
async execute(): Promise<AuthenticatedUser> { | ||
return await this.usersRepository.getCurrentAuthenticatedUser(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { UsersRepository } from './infra/repositories/UsersRepository'; | ||
import { GetCurrentAuthenticatedUser } from './domain/useCases/GetCurrentAuthenticatedUser'; | ||
|
||
const getCurrentAuthenticatedUser = new GetCurrentAuthenticatedUser(new UsersRepository()); | ||
|
||
export { getCurrentAuthenticatedUser }; | ||
export { AuthenticatedUser } from './domain/models/AuthenticatedUser'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'; | ||
import { IUsersRepository } from '../../domain/repositories/IUsersRepository'; | ||
import { AuthenticatedUser } from '../../domain/models/AuthenticatedUser'; | ||
import { AxiosResponse } from 'axios'; | ||
|
||
export class UsersRepository extends ApiRepository implements IUsersRepository { | ||
public async getCurrentAuthenticatedUser(): Promise<AuthenticatedUser> { | ||
return this.doGet('/users/:me', true) | ||
.then((response) => this.getAuthenticatedUserFromResponse(response)) | ||
.catch((error) => { | ||
throw error; | ||
}); | ||
} | ||
|
||
private getAuthenticatedUserFromResponse(response: AxiosResponse): AuthenticatedUser { | ||
const responseData = response.data.data; | ||
return { | ||
id: responseData.id, | ||
persistentUserId: responseData.persistentUserId, | ||
identifier: responseData.identifier, | ||
displayName: responseData.displayName, | ||
firstName: responseData.firstName, | ||
lastName: responseData.lastName, | ||
email: responseData.email, | ||
superuser: responseData.superuser, | ||
deactivated: responseData.deactivated, | ||
createdTime: responseData.createdTime, | ||
authenticationProviderId: responseData.authenticationProviderId, | ||
lastLoginTime: responseData.lastLoginTime, | ||
lastApiUseTime: responseData.lastApiUseTime, | ||
deactivatedTime: responseData.deactivatedTime, | ||
affiliation: responseData.affiliation, | ||
position: responseData.position, | ||
emailLastConfirmed: responseData.emailLastConfirmed, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { UsersRepository } from '../../../src/users/infra/repositories/UsersRepository'; | ||
import { ReadError } from '../../../src/core/domain/repositories/ReadError'; | ||
import { assert } from 'sinon'; | ||
import { ApiConfig } from '../../../src/core/infra/repositories/ApiConfig'; | ||
|
||
describe('getCurrentAuthenticatedUser', () => { | ||
// TODO: Change API URL to another of an integration test oriented Dataverse instance | ||
const sut: UsersRepository = new UsersRepository(); | ||
|
||
ApiConfig.init('https://demo.dataverse.org/api/v1'); | ||
|
||
test('should return error when authentication is not provided', async () => { | ||
let error: ReadError = undefined; | ||
await sut.getCurrentAuthenticatedUser().catch((e) => (error = e)); | ||
|
||
assert.match( | ||
error.message, | ||
'There was an error when reading the resource. Reason was: [400] User with token null not found.', | ||
); | ||
}); | ||
|
||
// TODO: Add more test cases once the integration test environment is established | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { AuthenticatedUser } from '../../../src/users/domain/models/AuthenticatedUser'; | ||
|
||
export const createAuthenticatedUser = (): AuthenticatedUser => { | ||
return { | ||
id: 1, | ||
persistentUserId: 'Test', | ||
identifier: '@Test', | ||
displayName: 'Test User', | ||
firstName: 'Testname', | ||
lastName: 'Testlastname', | ||
email: 'testuser@dataverse.org', | ||
superuser: false, | ||
deactivated: false, | ||
createdTime: '2023-04-14T11:52:28Z', | ||
authenticationProviderId: 'builtin', | ||
lastLoginTime: '2023-04-14T11:52:28Z', | ||
lastApiUseTime: '2023-04-14T15:53:32Z', | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { AuthRepository } from '../../../src/auth/infra/repositories/AuthRepository'; | ||
import { assert, createSandbox, SinonSandbox } from 'sinon'; | ||
import axios from 'axios'; | ||
import { expect } from 'chai'; | ||
import { ApiConfig } from '../../../src/core/infra/repositories/ApiConfig'; | ||
import { WriteError } from '../../../src/core/domain/repositories/WriteError'; | ||
|
||
describe('logout', () => { | ||
const sandbox: SinonSandbox = createSandbox(); | ||
const sut: AuthRepository = new AuthRepository(); | ||
const testApiUrl = 'https://test.dataverse.org/api/v1'; | ||
|
||
ApiConfig.init(testApiUrl); | ||
|
||
afterEach(() => { | ||
sandbox.restore(); | ||
}); | ||
|
||
test('should not return error on successful response', async () => { | ||
const testSuccessfulResponse = { | ||
data: { | ||
status: 'OK', | ||
data: { | ||
message: 'User logged out', | ||
}, | ||
}, | ||
}; | ||
const axiosPostStub = sandbox.stub(axios, 'post').resolves(testSuccessfulResponse); | ||
|
||
await sut.logout(); | ||
|
||
assert.calledWithExactly(axiosPostStub, `${testApiUrl}/logout`, JSON.stringify(''), { | ||
withCredentials: true, | ||
headers: { 'Content-Type': 'application/json' }, | ||
}); | ||
}); | ||
|
||
test('should return error result on error response', async () => { | ||
const testErrorResponse = { | ||
response: { | ||
status: 'ERROR', | ||
message: 'test', | ||
}, | ||
}; | ||
const axiosPostStub = sandbox.stub(axios, 'post').rejects(testErrorResponse); | ||
|
||
let error: WriteError = undefined; | ||
await sut.logout().catch((e) => (error = e)); | ||
|
||
assert.calledWithExactly(axiosPostStub, `${testApiUrl}/logout`, JSON.stringify(''), { | ||
withCredentials: true, | ||
headers: { 'Content-Type': 'application/json' }, | ||
}); | ||
expect(error).to.be.instanceOf(Error); | ||
}); | ||
}); |
Oops, something went wrong.