richie-education 3.3.1-dev5 → 3.3.1-dev8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/api/auth/keycloak.spec.ts +5 -4
- package/js/api/auth/keycloak.ts +2 -2
- package/js/api/authentication.ts +3 -0
- package/js/api/lms/openedx-fonzie-keycloak.spec.ts +79 -0
- package/js/api/lms/openedx-fonzie-keycloak.ts +106 -0
- package/js/types/api.ts +1 -0
- package/js/types/commonDataProps.ts +3 -2
- package/package.json +1 -1
|
@@ -44,8 +44,8 @@ jest.mock('utils/context', () => ({
|
|
|
44
44
|
authentication: {
|
|
45
45
|
backend: 'keycloak',
|
|
46
46
|
endpoint: 'https://keycloak.test/auth',
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
keycloak_client_id: 'richie-client',
|
|
48
|
+
keycloak_realm: 'richie-realm',
|
|
49
49
|
auth_url: 'https://keycloak.test/auth/realms/richie-realm/protocol/openid-connect/auth',
|
|
50
50
|
},
|
|
51
51
|
}).one(),
|
|
@@ -55,8 +55,9 @@ describe('Keycloak API', () => {
|
|
|
55
55
|
const authConfig = {
|
|
56
56
|
backend: 'keycloak',
|
|
57
57
|
endpoint: 'https://keycloak.test/auth',
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
keycloak_endpoint: 'https://keycloak.test/auth',
|
|
59
|
+
keycloak_client_id: 'richie-client',
|
|
60
|
+
keycloak_realm: 'richie-realm',
|
|
60
61
|
auth_url: 'https://keycloak.test/auth/realms/richie-realm/protocol/openid-connect/auth',
|
|
61
62
|
registration_url:
|
|
62
63
|
'https://keycloak.test/auth/realms/richie-realm/protocol/openid-connect/registrations',
|
package/js/api/auth/keycloak.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { RICHIE_USER_TOKEN } from 'settings';
|
|
|
9
9
|
const API = (APIConf: AuthenticationBackend): { user: APIAuthentication } => {
|
|
10
10
|
const keycloak = new Keycloak({
|
|
11
11
|
url: APIConf.endpoint,
|
|
12
|
-
realm: APIConf.
|
|
13
|
-
clientId: APIConf.
|
|
12
|
+
realm: APIConf.keycloak_realm!,
|
|
13
|
+
clientId: APIConf.keycloak_client_id!,
|
|
14
14
|
});
|
|
15
15
|
const initPromise = keycloak.init({
|
|
16
16
|
checkLoginIframe: false,
|
package/js/api/authentication.ts
CHANGED
|
@@ -15,6 +15,7 @@ import KeycloakApiInterface from './auth/keycloak';
|
|
|
15
15
|
import OpenEdxDogwoodApiInterface from './lms/openedx-dogwood';
|
|
16
16
|
import OpenEdxHawthornApiInterface from './lms/openedx-hawthorn';
|
|
17
17
|
import OpenEdxFonzieApiInterface from './lms/openedx-fonzie';
|
|
18
|
+
import OpenEdxFonzieKeycloakApiInterface from './lms/openedx-fonzie-keycloak';
|
|
18
19
|
|
|
19
20
|
const AuthenticationAPIHandler = (): Nullable<APIAuthentication> => {
|
|
20
21
|
const AUTHENTICATION: AuthenticationBackend = context?.authentication;
|
|
@@ -31,6 +32,8 @@ const AuthenticationAPIHandler = (): Nullable<APIAuthentication> => {
|
|
|
31
32
|
return OpenEdxHawthornApiInterface(AUTHENTICATION).user;
|
|
32
33
|
case APIBackend.FONZIE:
|
|
33
34
|
return OpenEdxFonzieApiInterface(AUTHENTICATION).user;
|
|
35
|
+
case APIBackend.FONZIE_KEYCLOAK:
|
|
36
|
+
return OpenEdxFonzieKeycloakApiInterface(AUTHENTICATION).user;
|
|
34
37
|
default:
|
|
35
38
|
handle(new Error(`No Authentication Backend found for ${AUTHENTICATION.backend}.`));
|
|
36
39
|
return null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import fetchMock from 'fetch-mock';
|
|
3
|
+
import { RICHIE_USER_TOKEN } from 'settings';
|
|
4
|
+
import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
|
|
5
|
+
import { location } from 'utils/indirection/window';
|
|
6
|
+
import FonzieKeycloakAPIInterface from './openedx-fonzie-keycloak';
|
|
7
|
+
|
|
8
|
+
jest.mock('utils/context', () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
default: null,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock('utils/indirection/window', () => ({
|
|
14
|
+
location: {
|
|
15
|
+
href: 'http://localhost/',
|
|
16
|
+
assign: jest.fn(),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('Fonzie Keycloak API', () => {
|
|
21
|
+
const configuration = {
|
|
22
|
+
backend: 'fonzie-keycloak',
|
|
23
|
+
course_regexp: '.*',
|
|
24
|
+
endpoint: 'https://demo.endpoint.api',
|
|
25
|
+
keycloak_endpoint: 'https://keycloak.test/auth',
|
|
26
|
+
keycloak_realm: 'richie-realm',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
fetchMock.restore();
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('uses its own route to get user information', async () => {
|
|
35
|
+
const user = {
|
|
36
|
+
username: faker.internet.username(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
fetchMock.get('https://demo.endpoint.api/api/v1.0/user/me', user);
|
|
40
|
+
|
|
41
|
+
const api = FonzieKeycloakAPIInterface(configuration);
|
|
42
|
+
await expect(api.user.me()).resolves.toEqual(user);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('redirects to keycloak-login endpoint on login', () => {
|
|
46
|
+
const api = FonzieKeycloakAPIInterface(configuration);
|
|
47
|
+
api.user.login!();
|
|
48
|
+
|
|
49
|
+
expect(location.assign).toHaveBeenCalledWith(
|
|
50
|
+
`https://demo.endpoint.api/keycloak-login?next=${encodeURIComponent('http://localhost/')}`,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses keycloak account URL to get user profile', async () => {
|
|
55
|
+
const openEdxApiProfile = OpenEdxApiProfileFactory().one();
|
|
56
|
+
const { 'pref-lang': language, ...account } = openEdxApiProfile;
|
|
57
|
+
|
|
58
|
+
fetchMock.get('https://keycloak.test/auth/realms/richie-realm/account/', account);
|
|
59
|
+
fetchMock.get(
|
|
60
|
+
`https://demo.endpoint.api/api/user/v1/preferences/${openEdxApiProfile.username}`,
|
|
61
|
+
{ 'pref-lang': language },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const api = FonzieKeycloakAPIInterface(configuration);
|
|
65
|
+
expect(await api.user.account!.get(openEdxApiProfile.username)).toEqual(openEdxApiProfile);
|
|
66
|
+
expect(fetchMock.calls()).toHaveLength(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('is able to retrieve access token within the session storage', () => {
|
|
70
|
+
const accessToken = faker.string.uuid();
|
|
71
|
+
sessionStorage.setItem(RICHIE_USER_TOKEN, accessToken);
|
|
72
|
+
|
|
73
|
+
const api = FonzieKeycloakAPIInterface(configuration);
|
|
74
|
+
expect(api.user.accessToken).not.toBeUndefined();
|
|
75
|
+
|
|
76
|
+
const token = api.user.accessToken!();
|
|
77
|
+
expect(token).toEqual(accessToken);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import Cookies from 'js-cookie';
|
|
2
|
+
import { AuthenticationBackend } from 'types/commonDataProps';
|
|
3
|
+
import { APILms } from 'types/api';
|
|
4
|
+
import { RICHIE_USER_TOKEN, EDX_CSRF_TOKEN_COOKIE_NAME } from 'settings';
|
|
5
|
+
import { isHttpError } from 'utils/errors/HttpError';
|
|
6
|
+
import { handle } from 'utils/errors/handle';
|
|
7
|
+
import { OpenEdxApiProfile } from 'types/openEdx';
|
|
8
|
+
import { checkStatus } from 'api/utils';
|
|
9
|
+
import { OpenEdxFullNameFormValues } from 'components/OpenEdxFullNameForm';
|
|
10
|
+
import { location } from 'utils/indirection/window';
|
|
11
|
+
import OpenEdxHawthornApiInterface from './openedx-hawthorn';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* OpenEdX completed by Fonzie API Implementation
|
|
16
|
+
*
|
|
17
|
+
* This implementation inherits from Hawthorn implementation.
|
|
18
|
+
* The `user.me` method has to be overriden to retrieve user information from
|
|
19
|
+
* fonzie API to retrieve a JWT Token
|
|
20
|
+
*
|
|
21
|
+
* A method `accessToken` has been added to retrieve the access_token
|
|
22
|
+
* stored in the persisted client by react query within SessionStorage.
|
|
23
|
+
*
|
|
24
|
+
* Related resources:
|
|
25
|
+
* https://github.com/openfun/fonzie/pull/24
|
|
26
|
+
*
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const API = (APIConf: AuthenticationBackend): APILms => {
|
|
30
|
+
const APIOptions = {
|
|
31
|
+
routes: {
|
|
32
|
+
user: {
|
|
33
|
+
login: `${APIConf.endpoint}/keycloak-login`,
|
|
34
|
+
me: `${APIConf.endpoint}/api/v1.0/user/me`,
|
|
35
|
+
account: `${APIConf.keycloak_endpoint}/realms/${APIConf.keycloak_realm}/account/`,
|
|
36
|
+
preferences: `${APIConf.endpoint}/api/user/v1/preferences/:username`,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const ApiInterface = OpenEdxHawthornApiInterface(APIConf, APIOptions);
|
|
42
|
+
return {
|
|
43
|
+
...ApiInterface,
|
|
44
|
+
user: {
|
|
45
|
+
...ApiInterface.user,
|
|
46
|
+
login: () => {
|
|
47
|
+
const next = encodeURIComponent(location.href);
|
|
48
|
+
location.assign(`${APIOptions.routes.user.login}?next=${next}`);
|
|
49
|
+
},
|
|
50
|
+
accessToken: () => {
|
|
51
|
+
return sessionStorage.getItem(RICHIE_USER_TOKEN);
|
|
52
|
+
},
|
|
53
|
+
account: {
|
|
54
|
+
get: async (username: string) => {
|
|
55
|
+
const options: RequestInit = {
|
|
56
|
+
credentials: 'include',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const account = await fetch(
|
|
61
|
+
APIOptions.routes.user.account.replace(':username', username),
|
|
62
|
+
options,
|
|
63
|
+
).then(checkStatus);
|
|
64
|
+
const preferences = await fetch(
|
|
65
|
+
APIOptions.routes.user.preferences.replace(':username', username),
|
|
66
|
+
options,
|
|
67
|
+
).then(checkStatus);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...account,
|
|
71
|
+
...preferences,
|
|
72
|
+
} as OpenEdxApiProfile;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (isHttpError(e)) {
|
|
75
|
+
handle(new Error(`[GET - Account] > ${e.code} - ${e.message}`));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
update: async (username: string, data: OpenEdxFullNameFormValues) => {
|
|
82
|
+
const csrfToken = Cookies.get(EDX_CSRF_TOKEN_COOKIE_NAME) || '';
|
|
83
|
+
try {
|
|
84
|
+
return await fetch(APIOptions.routes.user.account.replace(':username', username), {
|
|
85
|
+
method: 'PATCH',
|
|
86
|
+
credentials: 'include',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/merge-patch+json',
|
|
89
|
+
'X-CSRFTOKEN': csrfToken,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(data),
|
|
92
|
+
}).then(checkStatus);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (isHttpError(e)) {
|
|
95
|
+
handle(new Error(`[POST - Account] > ${e.code} - ${e.message}`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default API;
|
package/js/types/api.ts
CHANGED
|
@@ -14,8 +14,9 @@ export interface LMSBackend {
|
|
|
14
14
|
export interface AuthenticationBackend {
|
|
15
15
|
backend: string;
|
|
16
16
|
endpoint: string;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
keycloak_client_id?: string;
|
|
18
|
+
keycloak_endpoint?: string;
|
|
19
|
+
keycloak_realm?: string;
|
|
19
20
|
token?: string;
|
|
20
21
|
auth_url?: string;
|
|
21
22
|
registration_url?: string;
|