Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f77de2e
chore: prepare next development iteration
jenkinsEdificePublic Dec 5, 2025
e030415
chore: prepare next development iteration
jenkinsEdificePublic Nov 6, 2025
17eb23e
feat(app-registry):INTEG-812 add controller for ptit observatoire widget
theovgl Mar 19, 2025
8604ce2
chore(timeline): INTEG-422 add api for send notification from externa…
moustaphahennawi May 28, 2025
89d8464
feat(workspace): INTEG-896, port nextcloud integration directly into …
theovgl May 26, 2025
a2ec6bd
chore(workspace): Update trash bin label
theovgl Jun 16, 2025
7efe14b
chore(workspace): Remove share without copy option from toolbar
theovgl Jun 16, 2025
246cb29
feat(workspace): Add sync document viewer component to preview Nextcl…
theovgl Jun 17, 2025
39e4b54
Fix(workspace): various bugs and inconsistencies
theovgl Jun 18, 2025
954647d
INTEG-819-954: Ajout API temps d'écran
bassamkh Jul 1, 2025
4d8275c
Fix(workspace): INTEG-896 fix regression preventing nextcloud account…
theovgl Jul 8, 2025
db181ec
Feat(workspace): INTEG-897, open synced folder by URL param
theovgl Jul 25, 2025
928e69d
chore(oauth) : IW-209 enrichment of oauth2 connector logs
theovgl Sep 22, 2025
6c0f612
feat: #PEDAGO-3456, add multi broker client
nabil-mansouri Nov 4, 2025
2d6c0e9
feat: #PEDAGO-3456, avoid useless seralize
nabil-mansouri Nov 6, 2025
278d7e1
fix: add dummy listener for json generation
nabil-mansouri Nov 6, 2025
5d5e993
fix: do not encode twice a payload to publish to NATS (#928)
juniorode Nov 7, 2025
6fadee1
feat: add common interface for all generated BrokerListeners
juniorode Nov 8, 2025
aeada1d
feat: dynamically add and remove broker subjects (#929)
juniorode Nov 10, 2025
b45f42a
fix: remvoe unecessary dep on wikigen
juniorode Nov 13, 2025
b280b26
chore: put a slip on
juniorode Nov 13, 2025
efbdc9d
fix: listen on a queue for dynamically added broker subjects
juniorode Nov 14, 2025
63ae507
chore(portal): ENT13-7 add assistancetic key
moustaphahennawi Nov 14, 2025
f411a19
feat(auth): INTEG-1081 Add Google SAML service provider
theovgl Nov 17, 2025
0c815b3
fix: #PEDAGO-3456, dont serialize string twice
nabil-mansouri Nov 25, 2025
601e99c
chore: prepare next development iteration
jenkinsEdificePublic Dec 8, 2025
b838dee
chore: update ngx-ode-libs version to develop-pedago tag
david-cc Dec 8, 2025
0f4d177
chore: set snapshot version after merge
jenkinsEdificePublic Dec 10, 2025
cc7eec0
chore: prepare next development iteration
jenkinsEdificePublic Dec 10, 2025
e74e028
fix: better handling of vertx JsonObject + add missing property in na…
juniorode Dec 19, 2025
0fa3d38
chore: do not track generated clients
juniorode Dec 19, 2025
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
6 changes: 3 additions & 3 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ pipeline {
stage('Test') {
steps {
script {
// sh 'sleep 6'
try {
sh 'sleep 6'
/* try {
sh './build.sh $BUILD_SH_EXTRA_PARAM test'
} catch (err) {
}
}*/
}
}
}
Expand Down
1 change: 1 addition & 0 deletions admin/src/main/resources/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,7 @@
"rss-widget": "RSS",
"save": "Enregistrer",
"save.modifications": "Enregistrer les modifications",
"screen-time-widget": "Temps d'écran",
"scholarshipHolder": "Boursier",
"school-widget": "Mon réseau",
"search": "Rechercher",
Expand Down
10 changes: 5 additions & 5 deletions admin/src/main/ts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "admin-app",
"version": "6.11.0",
"version": "6.11-%branch%.%generateVersion%",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0",
Expand Down Expand Up @@ -34,9 +34,9 @@
"font-awesome": "4.7.0",
"jquery": "^3.4.1",
"ngx-infinite-scroll": "14.0.1",
"ngx-ode-core": "4.7.3",
"ngx-ode-sijil": "4.7.3",
"ngx-ode-ui": "4.7.3",
"ngx-ode-core": "dev",
"ngx-ode-sijil": "dev",
"ngx-ode-ui": "dev",
"ngx-trumbowyg": "^6.0.7",
"noty": "2.4.1",
"reflect-metadata": "0.1.10",
Expand Down Expand Up @@ -72,4 +72,4 @@
"typescript": "~4.6.4",
"webpack": "^5.70.0"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@

package org.entcore.registry;

import io.vertx.core.json.JsonObject;
import io.vertx.core.Promise;
import org.entcore.broker.api.utils.AddressParameter;
import org.entcore.broker.api.utils.BrokerProxyUtils;
import io.vertx.core.json.JsonObject;
import org.entcore.common.appregistry.AppRegistryEventsHandler;
import org.entcore.common.http.BaseServer;
import org.entcore.registry.controllers.*;
Expand Down Expand Up @@ -52,7 +52,12 @@ public void start(final Promise<Void> startPromise) throws Exception {
if(screenTimeEnabled != null) {
addController(new ScreenTimeController());
}


JsonObject ptitObservatoireConf = config.getJsonObject("ptit-observatoire-widget-config");
if (ptitObservatoireConf != null) {
addController(new PtitObservatoireController());
}

setDefaultResourceFilter(new AppRegistryFilter());
new AppRegistryEventsHandler(vertx, new NopAppRegistryEventService());
vertx.eventBus().publish("app-registry.loaded", new JsonObject());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package org.entcore.registry.controllers;

import fr.wseduc.rs.Get;
import fr.wseduc.rs.Post;
import fr.wseduc.security.ActionType;
import fr.wseduc.security.SecuredAction;
import fr.wseduc.webutils.http.BaseController;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.*;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.entcore.common.user.UserUtils;
import org.vertx.java.core.http.RouteMatcher;

import java.util.Map;

public class PtitObservatoireController extends BaseController {
private HttpClient httpClient;
private String ptitObservatoireUrl;
private String ptitObservatoireApiKey;

@Override
public void init(Vertx vertx, JsonObject config, RouteMatcher rm, Map<String, fr.wseduc.webutils.security.SecuredAction> securedActions) {
super.init(vertx, config, rm, securedActions);

this.httpClient = vertx.createHttpClient(new HttpClientOptions());
JsonObject ptitObservatoireConfiguration = config.getJsonObject("ptit-observatoire-widget-config");
this.ptitObservatoireUrl = ptitObservatoireConfiguration.getString("url", "");
this.ptitObservatoireApiKey = ptitObservatoireConfiguration.getString("api-key", "");
}

// Get the list of students related to a teacher.
@Get("/ptitObservatoire/students")
@SecuredAction(value = "", type = ActionType.AUTHENTICATED)
public void getPtitObservatoireStudents(final HttpServerRequest request) {
getIdLpo(request)
.compose(idLpo -> {
String url = ptitObservatoireUrl + "/students?filter[teacher_id]=" + idLpo;

RequestOptions options = new RequestOptions()
.setMethod(HttpMethod.GET)
.setAbsoluteURI(url)
.addHeader("X-API-KEY", ptitObservatoireApiKey)
.addHeader("Content-Type", "application/json");

return httpClient.request(options)
.compose(HttpClientRequest::send)
.compose(response -> {
Promise<JsonArray> promise = Promise.promise();
response.bodyHandler(body -> {
if (response.statusCode() == 200) {
JsonObject json = body.toJsonObject();
JsonArray data = json.getJsonArray("data", new JsonArray());
promise.complete(data);
} else {
promise.fail("Error from API: " + response.statusCode());
}
});
return promise.future();
});
})
.onSuccess(students -> renderJson(request, students))
.onFailure(err -> renderError(request, null, 500, err.getMessage()));
}

// Get the list of categories related to a teacher.
@Get("/ptitObservatoire/observations/categories")
@SecuredAction(value = "", type = ActionType.AUTHENTICATED)
public void getPtitObservatoireObservationsCategories(HttpServerRequest request) {
getIdLpo(request)
.compose(idLpo -> {
String url = ptitObservatoireUrl + "/observations/categories?filter[teacher_id]=" + idLpo;

RequestOptions options = new RequestOptions()
.setMethod(HttpMethod.GET)
.setAbsoluteURI(url)
.addHeader("X-API-KEY", ptitObservatoireApiKey)
.addHeader("Content-Type", "application/json");

return httpClient.request(options)
.compose(HttpClientRequest::send)
.compose(response -> {
Promise<JsonArray> promise = Promise.promise();
response.bodyHandler(body -> {
if (response.statusCode() == 200) {
JsonObject json = body.toJsonObject();
JsonArray data = json.getJsonArray("data", new JsonArray());
promise.complete(data);
} else {
promise.fail("Error from API: " + response.statusCode());
}
});
return promise.future();
});
})
.onSuccess(categories -> renderJson(request, categories))
.onFailure(err -> renderError(request, null, 500, err.getMessage()));
}

// Create a new observation.
@Post("/ptitObservatoire/observations")
@SecuredAction(value = "", type = ActionType.AUTHENTICATED)
public void createPtitObsevatoireObservation(HttpServerRequest request) {
Future<Buffer> bodyFuture = request.body();
Future<String> idLpoFuture = getIdLpo(request);

// Wait for both futures to complete
Future.all(bodyFuture, idLpoFuture)
.compose(cf -> {
Buffer body = cf.resultAt(0);
String idLpo = cf.resultAt(1);

JsonObject json = body.toJsonObject();

// Modify the json object to include the teacher_id in the body
JsonObject attributes = json
.getJsonObject("data", new JsonObject())
.getJsonObject("attributes", new JsonObject());
attributes.put("teacher_id", idLpo);

RequestOptions options = new RequestOptions()
.setMethod(HttpMethod.POST)
.setAbsoluteURI(ptitObservatoireUrl + "/observations")
.addHeader("X-API-KEY", ptitObservatoireApiKey)
.addHeader("Content-Type", "application/json");

return httpClient.request(options)
.compose(req -> req.send(json.encode()));
})
.onSuccess(response -> {
response.bodyHandler(buffer -> {
JsonObject resJson = buffer.toJsonObject();
if (response.statusCode() == 201) {
renderJson(request, resJson);
} else {
renderError(request, resJson);
}
});
})
.onFailure(err -> renderError(request, null, 500, err.getMessage()));
}

// Get the ID of a teacher in the PtitObservatoire API.
private Future<String> getIdLpo(HttpServerRequest request) {
return UserUtils.getAuthenticatedUserInfos(eb, request)
.compose(userInfos -> {
String userId = userInfos.getUserId();

String url = ptitObservatoireUrl + "/users?filter[external_id]=" + userId;

RequestOptions options = new RequestOptions()
.setMethod(HttpMethod.GET)
.setAbsoluteURI(url)
.addHeader("X-API-KEY", ptitObservatoireApiKey)
.addHeader("Content-Type", "application/json");

return httpClient.request(options)
.compose(HttpClientRequest::send)
.compose(response -> {
Promise<String> promise = Promise.promise();
response.bodyHandler(body -> {
if (response.statusCode() != 200) {
promise.fail("API error: " + response.statusCode());
return;
}

JsonArray data = body.toJsonObject().getJsonArray("data");
if (data == null || data.isEmpty()) {
promise.fail("User not found");
} else {
String idLpo = data.getJsonObject(0).getString("id");
promise.complete(idLpo);
}
});
return promise.future();
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ public void token(final HttpServerRequest request) {
@Override
public void handle(Void v) {
final Request req = new HttpServerRequestAdapter(request);
String clientId = req.getParameter("client_id");
String redirectUri = req.getParameter("redirect_uri");
log.info(getIp(request) +
" - Initialisation de connexion OAuth2 - ClientID " + clientId +
" - RedirectUri " + redirectUri);
token.handleRequest(req, new Handler<Response>() {

@Override
Expand Down
87 changes: 87 additions & 0 deletions auth/src/main/java/org/entcore/auth/services/impl/SSOGoogle.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.entcore.auth.services.impl;

import fr.wseduc.webutils.Either;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Handler;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import org.entcore.common.neo4j.Neo4j;
import org.entcore.common.neo4j.Neo4jResult;
import org.opensaml.saml2.core.Assertion;

public class SSOGoogle extends AbstractSSOProvider {
private static final Logger log = LoggerFactory.getLogger(SSOGoogle.class);
private static final String EMAIL = "email";
private static final String USERID = "userId";

@Override
public void generate(EventBus eb, String userId, String host, String serviceProviderEntityId,
Handler<Either<String, JsonArray>> handler) {
getEmail(userId)
.compose(this::fillResult)
.onSuccess(result -> handler.handle(new Either.Right<>(result)))
.onFailure(err -> {
log.error("[Auth@SSOGoogle::generate] Failed to generate response for Google: " + err.getMessage());
handler.handle(new Either.Left<>(err.getMessage()));
});
}

private Future<String> getEmail(String userId) {
Promise<String> promise = Promise.promise();

String query = "MATCH (u:User {id:{userId}})\n"
+ "OPTIONAL MATCH (u)-[:IN]->(:Group)-[:AUTHORIZED]->(:Role)-[:AUTHORIZE]->(:Action)<-[:PROVIDE]-(a:Application)\n"
+ "WITH u, COLLECT(a) AS applications\n"
+ "RETURN u.email AS email";

Neo4j.getInstance()
.execute(query, new JsonObject().put(USERID, userId), Neo4jResult.validUniqueResultHandler(event -> {
if (event.isLeft()) {
String err = event.left().getValue();
log.error("[Auth@SSOGoogle::getEmail] Neo4j error for user " + userId + " : " + err);
promise.fail(err);
return;
}

JsonObject row = event.right().getValue();
String email = row != null ? row.getString(EMAIL) : null;
if (email != null) {
email = email.trim();
}

if (email == null || email.isEmpty()) {
String msg = "User email not found";
log.warn("[Auth@SSOGoogle::getEmail] " + msg + " for user: " + userId);
promise.fail(msg);
return;
}

promise.complete(email);
}));

return promise.future();
}

private Future<JsonArray> fillResult(String email) {
Promise<JsonArray> promise = Promise.promise();
try {
JsonArray result = new JsonArray().add(new JsonObject().put(EMAIL, email));
promise.complete(result);
} catch (Exception e) {
log.error("[Auth@SSOGoogle::fillResult] Failed to build result: " + e.getMessage());
promise.fail(e.getMessage());
}
return promise.future();
}

@Override
public void execute(Assertion assertion, Handler<Either<String, Object>> handler) {
String errorMessage = "execute function not available on SSO Google implementation";
log.error("[Auth@SSOGoogle::execute] " + errorMessage);
handler.handle(new Either.Left<>(errorMessage));
}
}
1 change: 1 addition & 0 deletions broker-parent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
broker-client
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.entcore.broker.api;

import io.vertx.core.Future;

/**
* Interface of all ent listeners.
*/
public interface ENTBrokerListener {
/**
* Start listening.
* @return Future that completes when the listener is listening on NATS and ready to proceed messages.
*/
Future<Void> start();

/**
* Stop listening and releases resources.
* @return Future that completes when the listener stopped listening on NATS and released all allocated resources.
*/
Future<Void> stop();
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private static MessageConsumer<?> startListening(Listener listener, Object proxy
final Class<?> requestType = method.getParameterTypes()[0];
log.info("Start listening on address " + address);
return eb.consumer(address, (Handler<Message<byte[]>>) message -> {
log.debug("Received message on address " + address);
log.debug("Received message on address " + message.address());
Object response = null;
if (method.getParameterCount() == 1) {
final byte[] rawRequest = message.body();
Expand Down
Loading