Skip to content

Commit

Permalink
Merge branch 'dev' into pix-16318-fix-mastery-rate-display
Browse files Browse the repository at this point in the history
  • Loading branch information
matthiasferraina authored Jan 31, 2025
2 parents 659304f + f1392a2 commit f8dcd92
Show file tree
Hide file tree
Showing 51 changed files with 1,234 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export function buildChallenge({
locales = ['fr'],
competenceId = null,
skillId = null,
hasEmbedInternalValidation = false,
noValidationNeeded = false,
} = {}) {
return buildChallengeInDB({
id,
Expand Down Expand Up @@ -77,6 +79,8 @@ export function buildChallenge({
locales,
competenceId,
skillId,
hasEmbedInternalValidation,
noValidationNeeded,
});
}

Expand Down Expand Up @@ -118,6 +122,8 @@ export function buildChallengeWithNoDefaultValues({
locales,
competenceId,
skillId,
hasEmbedInternalValidation,
noValidationNeeded,
}) {
return buildChallengeInDB({
id,
Expand Down Expand Up @@ -157,6 +163,8 @@ export function buildChallengeWithNoDefaultValues({
locales,
competenceId,
skillId,
hasEmbedInternalValidation,
noValidationNeeded,
});
}

Expand Down Expand Up @@ -198,6 +206,8 @@ function buildChallengeInDB({
locales,
competenceId,
skillId,
hasEmbedInternalValidation,
noValidationNeeded,
}) {
const values = {
id,
Expand Down Expand Up @@ -237,6 +247,8 @@ function buildChallengeInDB({
locales,
competenceId,
skillId,
hasEmbedInternalValidation,
noValidationNeeded,
};
return databaseBuffer.pushInsertable({
tableName: 'learningcontent.challenges',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const SCHEMA_NAME = 'learningcontent';
const TABLE_NAME = 'challenges';
const COLUMN_NAME = 'hasEmbedInternalValidation';

const up = async function (knex) {
await knex.schema.withSchema(SCHEMA_NAME).table(TABLE_NAME, function (table) {
table
.boolean(COLUMN_NAME)
.defaultTo(false)
.comment('Indicates that the embed has internal rules to handle the challenge validation');
});
};

const down = async function (knex) {
await knex.schema.withSchema(SCHEMA_NAME).table(TABLE_NAME, function (table) {
table.dropColumn(COLUMN_NAME);
});
};

export { down, up };
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const SCHEMA_NAME = 'learningcontent';
const TABLE_NAME = 'challenges';
const COLUMN_NAME = 'noValidationNeeded';

const up = async function (knex) {
await knex.schema.withSchema(SCHEMA_NAME).table(TABLE_NAME, function (table) {
table
.boolean(COLUMN_NAME)
.defaultTo(false)
.comment(
'Indicates that the challenge does not need any validation, i.e. contains only a video to watch or a text to read',
);
});
};

const down = async function (knex) {
await knex.schema.withSchema(SCHEMA_NAME).table(TABLE_NAME, function (table) {
table.dropColumn(COLUMN_NAME);
});
};

export { down, up };
9 changes: 8 additions & 1 deletion api/lib/infrastructure/authentication.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import boom from '@hapi/boom';
import lodash from 'lodash';

import { revokedUserAccessRepository } from '../../src/identity-access-management/infrastructure/repositories/revoked-user-access.repository.js';
import { getForwardedOrigin } from '../../src/identity-access-management/infrastructure/utils/network.js';
import { config } from '../../src/shared/config.js';
import { tokenService } from '../../src/shared/domain/services/token-service.js';
Expand Down Expand Up @@ -90,7 +91,13 @@ async function _checkIsAuthenticated(request, h, { key, validate }) {

// Only tokens including user_id are User Access Tokens.
// This is why applications Access Tokens are not subject to audience validation for now.
if (decodedAccessToken.user_id && config.featureToggles.isUserTokenAudConfinementEnabled) {
const userId = decodedAccessToken.user_id;
if (config.featureToggles.isUserTokenAudConfinementEnabled && userId) {
const revokedUserAccess = await revokedUserAccessRepository.findByUserId(userId);
if (revokedUserAccess.isAccessTokenRevoked(decodedAccessToken)) {
return boom.unauthorized();
}

const audience = getForwardedOrigin(request.headers);
if (decodedAccessToken.aud !== audience) {
return boom.unauthorized();
Expand Down
11 changes: 11 additions & 0 deletions api/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,12 @@ REFRESH_TOKEN_LIFESPAN=7d
# default: '7d'
# REFRESH_TOKEN_LIFESPAN_PIX_ADMIN=7d

# Revoked user access lifespan
# presence: optional
# type: String
# default: '7d'
# REVOKED_USER_ACCESS_LIFESPAN=7d

# Saml access token lifespan
# presence: optional
# type: String
Expand Down Expand Up @@ -864,6 +870,11 @@ TEST_REDIS_URL=redis://localhost:6379
# default: false
# FT_USER_TOKEN_AUD_CONFINEMENT_ENABLED=false

# Enable new PixApp layout
# type: boolean
# default: false
# FT_PIXAPP_NEW_LAYOUT_ENABLED=false

# =====
# CPF
# =====
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,23 @@
},
{
"grainId": "4a80afba-d2ef-4f58-a328-a0fc9f204aca",
"content": "<p>Maintenant que vous savez déplacer votre souris, découvrez les différents boutons de la souris !<span aria-hidden=\"\\&quot;true\\&quot;\">🖱️️</span></p>"
"content": "<p>Maintenant que vous savez déplacer votre souris, découvrez les différents boutons de la souris !<span aria-hidden=\"true\">🖱️️</span></p>"
},
{
"grainId": "83f44f19-e8de-4248-bfc7-e38f19190138",
"content": "<p>Vous avez découvert le nom des boutons d'une souris.</p><p>Voyons à présent comment cliquer avec votre souris !<br></p>"
},
{
"grainId": "b491a99e-2774-469e-a63e-e2d0fb1120f2",
"content": "<p>Vous allez maintenant passer à la pratique. <span aria-hidden=\"\\&quot;true\\&quot;\">👇</span></p>"
"content": "<p>Vous allez maintenant passer à la pratique. <span aria-hidden=\"true\">👇</span></p>"
},
{
"grainId": "03e0657a-7caf-49eb-8dfb-da0f5e52b7ba",
"content": "<p>Il arrive que le pointeur de la souris change de forme. C’est tout à fait normal.&nbsp;</p><p>Découvrez les différentes formes de pointeur.<br><br></p>"
},
{
"grainId": "efba1e45-54ca-4829-b203-1e74b9269045",
"content": "<p>Le clic gauche sert à ouvrir des pages internet ou à démarrer des vidéos. Pour chacune de ses actions, le pointeur change de forme.</p>\n<p><span aria-hidden=\"\\&quot;true\\&quot;\">🖱️</span>️ À vous de jouer ! <br><br></p>"
"content": "<p>Le clic gauche sert à ouvrir des pages internet ou à démarrer des vidéos. Pour chacune de ses actions, le pointeur change de forme.</p><p><span aria-hidden=\"true\">🖱️</span>️ À vous de jouer !</p>"
}
],
"grains": [
Expand All @@ -72,7 +72,7 @@
"element": {
"id": "48708070-39c3-4af4-8914-ab61d61dfee8",
"type": "text",
"content": "<p>La souris fait partie de l'équipement de base d’un ordinateur, avec le clavier.</p>\n<p>Les souris peuvent avoir différentes formes, tailles ou couleurs. </p>\n<p>Il y a deux types de souris : </p>\n<ul>\n <li>les souris avec un fil</li>\n <li>les souris sans fil</li>\n</ul>\n<p>Globalement, toutes ces souris servent à faire les mêmes choses.</p>"
"content": "<p>La souris fait partie de l'équipement de base d’un ordinateur, avec le clavier.</p><p>Les souris peuvent avoir différentes formes, tailles ou couleurs. </p><p>Il y a deux types de souris : </p><ul><li>les souris avec un fil</li><li>les souris sans fil</li></ul><p>Globalement, toutes ces souris servent à faire les mêmes choses.</p>"
}
},
{
Expand All @@ -82,7 +82,7 @@
"type": "image",
"url": "https://i.imgur.com/0bI4zEN.png",
"alt": "-",
"alternativeText": "<ul><li>Les souris avec un fil : photographies de 4 souris avec fil de taille, forme et couleur différentes.</li><li>Les souris sans fil : photographies de 3 souris sans fil&nbsp;différentes reliées par des connecteurs USB Bluetooth .</li></ul>"
"alternativeText": "<ul><li>Les souris avec un fil : photographies de 4 souris avec fil de taille, forme et couleur différentes.</li><li>Les souris sans fil : photographies de 3 souris sans fil différentes reliées par des connecteurs USB Bluetooth .</li></ul>"
}
},
{
Expand Down Expand Up @@ -111,9 +111,13 @@
{
"type": "element",
"element": {
"id": "c2f89648-1f06-4e58-b5a5-422677d24417",
"type": "text",
"content": "<iframe width=\"700\" height=\"410\" src=\"https://www.youtube.com/embed/oGwHYZcs5fE\" title=\"Première marche souris - Prise en main de la souris\"></iframe>"
"id": "56541ee2-cf5f-4966-b17b-96a1b7266868",
"type": "video",
"title": "Prendre en main la souris",
"url": "https://videos.pix.fr/modulix/utiliser-souris-ordinateur-1/souris_prise_en_main.mp4",
"poster": "https://i.imgur.com/lA2jUAY.jpeg",
"subtitles": "",
"transcription": ""
}
}
]
Expand Down Expand Up @@ -164,9 +168,13 @@
{
"type": "element",
"element": {
"id": "52a71515-a085-4a85-8c43-86e06c1cb9b8",
"type": "text",
"content": "<iframe width=\"100%\" height=\"410\" src=\"https://www.youtube.com/embed/cktRk8PySII\" title=\"Première marche souris - Déplacer la souris\"></iframe>"
"id": "abe223ab-ad36-4b14-b349-abc8590eb94e",
"type": "video",
"title": "Déplacer la souris",
"url": "https://videos.pix.fr/modulix/utiliser-souris-ordinateur-1/deplacer_la_souris.mp4",
"poster": "https://i.imgur.com/jC8keAF.jpeg",
"subtitles": "",
"transcription": ""
}
}
]
Expand Down Expand Up @@ -266,7 +274,7 @@
"element": {
"id": "98a59942-5506-4c18-b16a-97ce2010103b",
"type": "text",
"content": "<p><strong><span aria-hidden=\"\\&quot;true\\&quot;\">🔎</span> Observez : combien de boutons a votre souris d’ordinateur ?</strong></p>"
"content": "<p><strong><span aria-hidden=\"true\">🔎</span> Observez : combien de boutons a votre souris d’ordinateur ?</strong></p>"
}
}
]
Expand Down Expand Up @@ -314,7 +322,7 @@
"element": {
"id": "a510f893-7d93-4e4f-b8a4-0dd978324c70",
"type": "text",
"content": "<p>Voyons comment faire un clic gauche. <span aria-hidden=\"\\&quot;true\\&quot;\">👇</span></p>"
"content": "<p>Voyons comment faire un clic gauche. <span aria-hidden=\"true\">👇</span></p>"
}
},
{
Expand Down Expand Up @@ -395,7 +403,7 @@
"type": "image",
"url": "https://i.imgur.com/G6t4f0c.png",
"alt": "",
"alternativeText": "<p>image des 4 formes de pointeur les plus courantes : la flèche, la main, la barre verticale, et la roue ou le sablier.&nbsp;</p>"
"alternativeText": "<p>image des 4 formes de pointeur les plus courantes : la flèche, la main, la barre verticale, et la roue ou le sablier.</p>"
}
},
{
Expand Down Expand Up @@ -489,7 +497,7 @@
"element": {
"id": "3480a769-41ea-47e8-990b-c5f8089542e7",
"type": "text",
"content": "<ul>\n <li><span aria-hidden=\"\\&quot;true\\&quot;\">🖐️</span>La souris glisse sur la table grâce à votre main.</li>\n <li><span aria-hidden=\"\\&quot;true\\&quot;\">🖥️</span> À l'écran, le pointeur suit le mouvement de la souris.</li>\n <li><span aria-hidden=\"\\&quot;true\\&quot;\">👉</span> Cliquer veut dire appuyer brièvement sur un bouton de la souris.</li>\n <li><span aria-hidden=\"\\&quot;true\\&quot;\">▶️</span> Cliquer permet d'interagir avec l’ordinateur.</li>\n <li><span aria-hidden=\"\\&quot;true\\&quot;\">🖱️Le pointeur de la souris change de forme quand on peut cliquer.</span></li>\n</ul>"
"content": "<ul> <li><span aria-hidden=\"true\">🖐️</span>La souris glisse sur la table grâce à votre main.</li> <li><span aria-hidden=\"true\">🖥️</span> À l'écran, le pointeur suit le mouvement de la souris.</li> <li><span aria-hidden=\"true\">👉</span> Cliquer veut dire appuyer brièvement sur un bouton de la souris.</li> <li><span aria-hidden=\"true\">▶️</span> Cliquer permet d'interagir avec l’ordinateur.</li> <li><span aria-hidden=\"true\">🖱️Le pointeur de la souris change de forme quand on peut cliquer.</span></li></ul>"
}
},
{
Expand Down
14 changes: 14 additions & 0 deletions api/src/identity-access-management/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ class UserCantBeCreatedError extends DomainError {
}
}

class RevokeUntilMustBeAnInstanceOfDate extends DomainError {
constructor(message = 'Revoke Until must be an instance of Date') {
super(message);
}
}

class UserIdIsRequiredError extends DomainError {
constructor(message = 'User Id is required') {
super(message);
}
}

class UserShouldChangePasswordError extends DomainError {
constructor(message = 'User password must be changed.', meta) {
super(message);
Expand All @@ -87,6 +99,8 @@ export {
OrganizationLearnerNotBelongToOrganizationIdentityError,
PasswordNotMatching,
PasswordResetDemandNotFoundError,
RevokeUntilMustBeAnInstanceOfDate,
UserCantBeCreatedError,
UserIdIsRequiredError,
UserShouldChangePasswordError,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class RevokedUserAccess {
constructor(revokeTimeStamp) {
this.revokeTimeStamp = revokeTimeStamp;
}
isAccessTokenRevoked(decodedToken) {
const issuedAt = decodedToken.iat;
if (!this.revokeTimeStamp) {
return false;
}
return issuedAt < this.revokeTimeStamp;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { config } from '../../../../src/shared/config.js';
import { temporaryStorage } from '../../../../src/shared/infrastructure/key-value-storages/index.js';
import { UserIdIsRequiredError } from '../../domain/errors.js';
import { RevokeUntilMustBeAnInstanceOfDate } from '../../domain/errors.js';
import { RevokedUserAccess } from '../../domain/models/RevokedUserAccess.js';

const revokedUserAccessTemporaryStorage = temporaryStorage.withPrefix('revoked-user-access:');
const revokedUserAccessLifespanMs = config.authentication.revokedUserAccessLifespanMs;

const saveForUser = async function (userId, revokeUntil) {
if (!userId) {
throw new UserIdIsRequiredError();
}

if (!(revokeUntil instanceof Date)) {
throw new RevokeUntilMustBeAnInstanceOfDate();
}

await revokedUserAccessTemporaryStorage.save({
key: userId,
value: Math.floor(revokeUntil.getTime() / 1000),
expirationDelaySeconds: revokedUserAccessLifespanMs / 1000,
});
};

const findByUserId = async function (userId) {
const value = await revokedUserAccessTemporaryStorage.get(userId);
return new RevokedUserAccess(value);
};

export const revokedUserAccessRepository = { saveForUser, findByUserId };
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class ChallengeRepository extends LearningContentRepository {
locales,
competenceId,
skillId,
hasEmbedInternalValidation,
noValidationNeeded,
}) {
return {
id,
Expand Down Expand Up @@ -86,6 +88,8 @@ class ChallengeRepository extends LearningContentRepository {
locales,
competenceId,
skillId,
hasEmbedInternalValidation,
noValidationNeeded,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import stream from 'node:stream';

import { tokenService } from '../../../shared/domain/services/token-service.js';
import { extractLocaleFromRequest } from '../../../shared/infrastructure/utils/request-response-utils.js';
import { escapeFileName } from '../../../shared/infrastructure/utils/request-response-utils.js';
import { usecases } from '../domain/usecases/index.js';
import * as campaignDetailsManagementSerializer from '../infrastructure/serializers/jsonapi/campaign-management-serializer.js';
Expand All @@ -14,12 +15,14 @@ const getByCode = async function (
request,
_,
dependencies = {
extractLocaleFromRequest,
campaignToJoinSerializer,
},
) {
const { code } = request.query.filter;
const locale = dependencies.extractLocaleFromRequest(request);

const campaignToJoin = await usecases.getCampaignByCode({ code });
const campaignToJoin = await usecases.getCampaignByCode({ code, locale });
return dependencies.campaignToJoinSerializer.serialize(campaignToJoin);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class CampaignToJoin extends Campaign {

this.isRestricted = organizationIsManagingStudents || hasLearnersImportFeature;
this.reconciliationFields = null;
this.isMobileCompliant = null;
this.isTabletCompliant = null;
}

get isReconciliationRequired() {
Expand All @@ -44,6 +46,11 @@ class CampaignToJoin extends Campaign {
setReconciliationFields(reconciliationFields) {
this.reconciliationFields = reconciliationFields;
}

setMediaCompliance({ isMobileCompliant, isTabletCompliant }) {
this.isMobileCompliant = isMobileCompliant;
this.isTabletCompliant = isTabletCompliant;
}
}

export { CampaignToJoin };
Loading

0 comments on commit f8dcd92

Please sign in to comment.