Java tutorial
/* Copyright "Open Digital Education", 2014 * * This program is published by "Open Digital Education". * You must indicate the name of the software and the company in any production /contribution * using the software and indicate on the home page of the software industry in question, * "powered by Open Digital Education" with a reference to the website: https://opendigitaleducation.com/. * * This program is free software, licensed under the terms of the GNU Affero General Public License * as published by the Free Software Foundation, version 3 of the License. * * You can redistribute this application and/or modify it since you respect the terms of the GNU Affero General Public License. * If you modify the source code and then use this modified source code in your creation, you must make available the source code of your modifications. * * You should have received a copy of the GNU Affero General Public License along with the software. * If not, please see : <http://www.gnu.org/licenses/>. Full compliance requires reading the terms of this license and following its directives. * */ package org.entcore.auth.users; import java.util.Date; import java.util.HashMap; import java.util.Map; import fr.wseduc.webutils.Either; import fr.wseduc.webutils.I18n; import fr.wseduc.webutils.email.EmailSender; import org.entcore.auth.pojo.SendPasswordDestination; import io.vertx.core.shareddata.LocalMap; import org.entcore.common.email.EmailFactory; import org.joda.time.DateTime; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.EventBus; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import org.entcore.common.neo4j.Neo; import org.entcore.common.neo4j.Neo4jResult; import org.entcore.common.validation.StringValidation; import fr.wseduc.webutils.Server; import fr.wseduc.webutils.http.Renders; import fr.wseduc.webutils.security.BCrypt; import static fr.wseduc.webutils.Utils.getOrElse; import static fr.wseduc.webutils.Utils.handlerToAsyncHandler; import static fr.wseduc.webutils.Utils.isNotEmpty; public class DefaultUserAuthAccount implements UserAuthAccount { private final Neo neo; private final Vertx vertx; private final JsonObject config; private final EmailSender notification; private final Renders render; private String smsProvider; private final String smsAddress; private final JsonArray allowActivateDuplicateProfiles; public DefaultUserAuthAccount(Vertx vertx, JsonObject config) { EventBus eb = Server.getEventBus(vertx); this.neo = new Neo(vertx, eb, null); this.vertx = vertx; this.config = config; EmailFactory emailFactory = new EmailFactory(vertx, config); notification = emailFactory.getSender(); render = new Renders(vertx, config); LocalMap<Object, Object> server = vertx.sharedData().getLocalMap("server"); if (server != null && server.get("smsProvider") != null) { smsProvider = (String) server.get("smsProvider"); final String node = (String) server.get("node"); smsAddress = (node != null ? node : "") + "entcore.sms"; } else { smsAddress = "entcore.sms"; } this.allowActivateDuplicateProfiles = getOrElse(config.getJsonArray("allow-activate-duplicate"), new JsonArray().add("Relative")); } @Override public void activateAccount(final String login, String activationCode, final String password, String email, String phone, final String theme, final HttpServerRequest request, final Handler<Either<String, String>> handler) { activateAccount("login", login, activationCode, password, email, phone, theme, request, handler); } @Override public void activateAccountByLoginAlias(final String login, String activationCode, final String password, String email, String phone, final String theme, final HttpServerRequest request, final Handler<Either<String, String>> handler) { activateAccount("loginAlias", login, activationCode, password, email, phone, theme, request, handler); } private void activateAccount(final String loginFieldName, final String login, String activationCode, final String password, String email, String phone, final String theme, final HttpServerRequest request, final Handler<Either<String, String>> handler) { String query = "MATCH (n:User) " + "WHERE n." + loginFieldName + "={login} AND n.activationCode = {activationCode} AND n.password IS NULL " + "AND (NOT EXISTS(n.blocked) OR n.blocked = false) " + "OPTIONAL MATCH n-[r:DUPLICATE]-() " + "WHERE NOT(head(n.profiles) IN {allowActivateDuplicate}) " + "OPTIONAL MATCH (p:Profile) " + "WHERE HAS(n.profiles) AND p.name = head(n.profiles) " + "WITH n, LENGTH(FILTER(x IN COLLECT(distinct r.score) WHERE x > 3)) as duplicates, p.blocked as blockedProfile " + "WHERE (blockedProfile IS NULL OR blockedProfile = false) " + "FOREACH (duplicate IN CASE duplicates WHEN 0 THEN [1] ELSE [] END | " + "SET n.password = {password}, n.activationCode = null, n.email = {email}, n.mobile = {phone}) " + "RETURN n.password as password, n.id as id, HEAD(n.profiles) as profile, duplicates > 0 as hasDuplicate "; Map<String, Object> params = new HashMap<>(); params.put("login", login); params.put("activationCode", activationCode); params.put("password", BCrypt.hashpw(password, BCrypt.gensalt())); params.put("email", email); params.put("phone", phone); params.put("allowActivateDuplicate", allowActivateDuplicateProfiles); neo.send(query, params, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> res) { if ("ok".equals(res.body().getString("status")) && res.body().getJsonObject("result").getJsonObject("0") != null) { if (res.body().getJsonObject("result").getJsonObject("0").getBoolean("hasDuplicate")) { handler.handle(new Either.Left<String, String>("activation.error.duplicated")); return; } JsonObject jo = new JsonObject() .put("userId", res.body().getJsonObject("result").getJsonObject("0").getString("id")) .put("profile", res.body().getJsonObject("result").getJsonObject("0").getString("profile")) .put("request", new JsonObject().put("headers", new JsonObject().put("Accept-Language", I18n.acceptLanguage(request)) .put("Host", Renders.getHost(request)))); if (isNotEmpty(theme)) { jo.put("theme", theme); } Server.getEventBus(vertx).publish("activation.ack", jo); handler.handle(new Either.Right<String, String>( res.body().getJsonObject("result").getJsonObject("0").getString("id"))); } else { String q = "MATCH (n:User) " + "WHERE n." + loginFieldName + "={login} AND n.activationCode IS NULL " + "AND NOT(n.password IS NULL) " + "RETURN n.password as password, n.id as id"; Map<String, Object> p = new HashMap<>(); p.put("login", login); neo.send(q, p, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> event) { if ("ok".equals(event.body().getString("status")) && event.body().getJsonObject("result").getJsonObject("0") != null && BCrypt.checkpw(password, event.body().getJsonObject("result") .getJsonObject("0").getString("password", ""))) { handler.handle(new Either.Right<String, String>( event.body().getJsonObject("result").getJsonObject("0").getString("id"))); } else { handler.handle(new Either.Left<String, String>("activation.error")); } } }); } } }); } @Override public void matchActivationCode(final String login, String potentialActivationCode, final Handler<Boolean> handler) { matchActivationCode("login", login, potentialActivationCode, handler); } @Override public void matchActivationCodeByLoginAlias(final String login, String potentialActivationCode, final Handler<Boolean> handler) { matchActivationCode("loginAlias", login, potentialActivationCode, handler); } private void matchActivationCode(final String loginFieldName, final String login, String potentialActivationCode, final Handler<Boolean> handler) { String query = "MATCH (n:User) " + "WHERE n." + loginFieldName + "={login} AND n.activationCode = {activationCode} AND n.password IS NULL " + "AND (NOT EXISTS(n.blocked) OR n.blocked = false) " + "RETURN true as exists"; JsonObject params = new JsonObject().put("login", login).put("activationCode", potentialActivationCode); neo.execute(query, params, Neo4jResult.validUniqueResultHandler(new Handler<Either<String, JsonObject>>() { @Override public void handle(Either<String, JsonObject> event) { if (event.isLeft() || !event.right().getValue().getBoolean("exists", false)) handler.handle(false); else handler.handle(true); } })); } @Override public void matchResetCode(final String login, String potentialResetCode, final Handler<Boolean> handler) { matchResetCode("login", login, potentialResetCode, handler); } @Override public void matchResetCodeByLoginAlias(final String login, String potentialResetCode, final Handler<Boolean> handler) { matchResetCode("loginAlias", login, potentialResetCode, handler); } private void matchResetCode(final String loginFieldName, final String login, String potentialResetCode, final Handler<Boolean> handler) { String query = "MATCH (n:User) " + "WHERE n." + loginFieldName + "={login} AND n.resetCode = {resetCode} " + "RETURN true as exists"; JsonObject params = new JsonObject().put("login", login).put("resetCode", potentialResetCode); neo.execute(query, params, Neo4jResult.validUniqueResultHandler(new Handler<Either<String, JsonObject>>() { @Override public void handle(Either<String, JsonObject> event) { if (event.isLeft() || !event.right().getValue().getBoolean("exists", false)) handler.handle(false); else handler.handle(true); } })); } @Override public void findByMail(final String email, final Handler<Either<String, JsonObject>> handler) { String query = "MATCH (u:User) WHERE u.email = {mail} AND u.activationCode IS NULL RETURN u.login as login, u.mobile as mobile"; JsonObject params = new JsonObject().put("mail", email); neo.execute(query, params, Neo4jResult.validUniqueResultHandler(handler)); } @Override public void findByMailAndFirstNameAndStructure(final String email, String firstName, String structure, final Handler<Either<String, JsonArray>> handler) { boolean setFirstname = firstName != null && !firstName.trim().isEmpty(); boolean setStructure = structure != null && !structure.trim().isEmpty(); String query = "MATCH (u:User)-[:IN]->(sg:Group)-[:DEPENDS]->(s:Structure) WHERE u.email = {mail} " + (setFirstname ? " AND u.firstName =~ {firstName}" : "") + (setStructure ? " AND s.id = {structure}" : "") + " AND u.activationCode IS NULL RETURN DISTINCT u.login as login, u.mobile as mobile, s.name as structureName, s.id as structureId"; JsonObject params = new JsonObject().put("mail", email); if (setFirstname) params.put("firstName", "(?i)" + firstName); if (setStructure) params.put("structure", structure); neo.execute(query, params, Neo4jResult.validResultHandler(handler)); } @Override public void findByLogin(final String login, final String resetCode, boolean checkFederatedLogin, final Handler<Either<String, JsonObject>> handler) { boolean setResetCode = resetCode != null && !resetCode.trim().isEmpty(); String basicQuery = "MATCH (n:User) " + "WHERE (n.login={login} OR n.loginAlias={login}) " + "AND n.activationCode IS NULL " + (checkFederatedLogin ? "AND (NOT(HAS(n.federated)) OR n.federated = false) " : "") + (setResetCode ? "SET n.resetCode = {resetCode}, n.resetDate = {today} " : "") + "RETURN n.email as email, n.mobile as mobile"; final String teacherQuery = "MATCH (n:User)-[:IN]->(sg:ProfileGroup)-[:DEPENDS]->(m:Class)" + "<-[:DEPENDS]-(tg:ProfileGroup)<-[:IN]-(p:User), " + "sg-[:DEPENDS]->(psg:ProfileGroup)-[:HAS_PROFILE]->(sp:Profile {name:'Student'}), " + "tg-[:DEPENDS]->(ptg:ProfileGroup)-[:HAS_PROFILE]->(tp:Profile {name:'Teacher'}) " + "WHERE (n.login={login} OR n.loginAlias={login}) AND NOT(p.email IS NULL) AND n.activationCode IS NULL AND " + "(NOT(HAS(n.federated)) OR n.federated = false) " + (setResetCode ? "SET n.resetCode = {resetCode}, n.resetDate = {today} " : "") + "RETURN p.email as email"; final JsonObject params = new JsonObject().put("login", login); if (setResetCode) params.put("resetCode", resetCode).put("today", new Date().getTime()); neo.execute(basicQuery, params, Neo4jResult.validUniqueResultHandler(new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> result) { if (result.isLeft()) { handler.handle(result); return; } final String mail = result.right().getValue().getString("email"); final String mobile = result.right().getValue().getString("mobile"); if (mail != null && config.getBoolean("teacherForgotPasswordEmail", false)) { neo.execute(teacherQuery, params, Neo4jResult.validUniqueResultHandler(new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> resultTeacher) { if (resultTeacher.isLeft()) { handler.handle(resultTeacher); return; } resultTeacher.right().getValue().put("mobile", mobile); handler.handle(resultTeacher); } })); } handler.handle(result); } })); } @Override public void sendResetPasswordMail(HttpServerRequest request, String email, String resetCode, final Handler<Either<String, JsonObject>> handler) { if (email == null || resetCode == null || email.trim().isEmpty() || resetCode.trim().isEmpty()) { handler.handle(new Either.Left<String, JsonObject>("invalid.mail")); return; } JsonObject json = new JsonObject().put("host", notification.getHost(request)).put("resetUri", notification.getHost(request) + "/auth/reset/" + resetCode); notification.sendEmail(request, email, config.getString("email", "noreply@one1d.fr"), null, null, "mail.reset.pw.subject", "email/forgotPassword.html", json, true, handlerToAsyncHandler(new Handler<Message<JsonObject>>() { public void handle(Message<JsonObject> event) { if ("error".equals(event.body().getString("status"))) { handler.handle( new Either.Left<String, JsonObject>(event.body().getString("message", ""))); } else { handler.handle(new Either.Right<String, JsonObject>(event.body())); } } })); } @Override public void sendForgottenIdMail(HttpServerRequest request, String login, String email, final Handler<Either<String, JsonObject>> handler) { if (email == null || email.trim().isEmpty()) { handler.handle(new Either.Left<String, JsonObject>("invalid.mail")); return; } JsonObject json = new JsonObject().put("login", login).put("host", notification.getHost(request)); notification.sendEmail(request, email, config.getString("email", "noreply@one1d.fr"), null, null, "mail.reset.id.subject", "email/forgotId.html", json, true, handlerToAsyncHandler(new Handler<Message<JsonObject>>() { public void handle(Message<JsonObject> event) { if ("error".equals(event.body().getString("status"))) { handler.handle( new Either.Left<String, JsonObject>(event.body().getString("message", ""))); } else { handler.handle(new Either.Right<String, JsonObject>(event.body())); } } })); } private void sendSms(HttpServerRequest request, final String phone, String template, JsonObject params, final Handler<Either<String, JsonObject>> handler) { if (phone == null || phone.trim().isEmpty()) { handler.handle(new Either.Left<String, JsonObject>("invalid.phone")); return; } final String formattedPhone = StringValidation.formatPhone(phone); render.processTemplate(request, template, params, new Handler<String>() { @Override public void handle(String body) { if (body != null) { JsonObject smsObject = new JsonObject().put("provider", smsProvider).put("action", "send-sms") .put("parameters", new JsonObject() .put("receivers", new fr.wseduc.webutils.collections.JsonArray() .add(formattedPhone)) .put("message", body).put("senderForResponse", true) .put("noStopClause", true)); vertx.eventBus().send(smsAddress, smsObject, handlerToAsyncHandler(new Handler<Message<JsonObject>>() { public void handle(Message<JsonObject> event) { if ("error".equals(event.body().getString("status"))) { handler.handle(new Either.Left<String, JsonObject>( event.body().getString("message", ""))); } else { handler.handle(new Either.Right<String, JsonObject>(event.body())); } } })); } else { handler.handle(new Either.Left<String, JsonObject>("template.error")); } } }); } @Override public void sendResetPasswordSms(HttpServerRequest request, String phone, String resetCode, final Handler<Either<String, JsonObject>> handler) { JsonObject params = new JsonObject().put("resetCode", resetCode).put("resetUri", resetCode); sendSms(request, phone, "phone/forgotPassword.txt", params, handler); } @Override public void sendForgottenIdSms(HttpServerRequest request, String login, String phone, final Handler<Either<String, JsonObject>> handler) { JsonObject params = new JsonObject().put("login", login); sendSms(request, phone, "phone/forgotId.txt", params, handler); } @Override public void resetPassword(String login, String resetCode, String password, final Handler<Boolean> handler) { final long codeDelay = config.getInteger("resetCodeDelay", 0); String query = "MATCH (n:User) " + "WHERE n.login={login} AND n.resetCode = {resetCode} " + (codeDelay > 0 ? "AND coalesce({today} - n.resetDate < {delay}, true) " : "") + "SET n.password = {password}, n.resetCode = null, n.resetDate = null " + "RETURN n.password as pw"; Map<String, Object> params = new HashMap<>(); params.put("login", login); params.put("resetCode", resetCode); if (codeDelay > 0) { params.put("today", new Date().getTime()); params.put("delay", codeDelay); } updatePassword(handler, query, password, params); } @Override public void changePassword(String login, String password, final Handler<Boolean> handler) { String query = "MATCH (n:User) " + "WHERE n.login={login} AND NOT(n.password IS NULL) " + "SET n.password = {password} " + "RETURN n.password as pw"; Map<String, Object> params = new HashMap<>(); params.put("login", login); updatePassword(handler, query, password, params); } @Override public void sendResetCode(final HttpServerRequest request, final String login, final SendPasswordDestination dest, boolean checkFederatedLogin, final Handler<Boolean> handler) { String query = "MATCH (n:User) " + "WHERE n.login={login} AND n.activationCode IS NULL " + (checkFederatedLogin ? "AND (NOT(HAS(n.federated)) OR n.federated = false) " : "") + "SET n.resetCode = {resetCode}, n.resetDate = {today} " + "RETURN count(n) as nb"; final String code = StringValidation.generateRandomCode(8); JsonObject params = new JsonObject().put("login", login).put("resetCode", code).put("today", new Date().getTime()); neo.execute(query, params, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> event) { if ("ok".equals(event.body().getString("status")) && event.body().getJsonArray("result") != null && event.body().getJsonArray("result").size() == 1 && 1 == ((JsonObject) event.body().getJsonArray("result").getJsonObject(0)) .getInteger("nb")) { if ("email".equals(dest.getType())) { sendResetPasswordMail(request, dest.getValue(), code, new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> event) { handler.handle(event.isRight()); } }); } else if ("mobile".equals(dest.getType())) { sendResetPasswordSms(request, dest.getValue(), code, new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> event) { handler.handle(event.isRight()); } }); } else { handler.handle(false); } } else { handler.handle(false); } } }); } @Override public void blockUser(String id, boolean block, final Handler<Boolean> handler) { String query = "MATCH (n:`User` { id : {id}}) SET n.blocked = {block} return count(*) = 1 as exists"; JsonObject params = new JsonObject().put("id", id).put("block", block); neo.execute(query, params, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> r) { handler.handle("ok".equals(r.body().getString("status")) && r.body().getJsonArray("result") != null && r.body().getJsonArray("result").getValue(0) != null && (r.body().getJsonArray("result").getJsonObject(0)).getBoolean("exists", false)); } }); } @Override public void storeDomain(String id, String domain, String scheme, final Handler<Boolean> handler) { String query = "MATCH (u:User {id: {id}}) " + "SET u.lastDomain = {domain}, u.lastScheme = {scheme}, u.lastLogin = {now} " + "return count(*) = 1 as exists"; JsonObject params = new JsonObject().put("id", id).put("domain", domain).put("scheme", scheme).put("now", DateTime.now().toString()); neo.execute(query, params, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> r) { handler.handle("ok".equals(r.body().getString("status")) && r.body().getJsonArray("result") != null && r.body().getJsonArray("result").getValue(0) != null && (r.body().getJsonArray("result").getJsonObject(0)).getBoolean("exists", false)); } }); }; private void updatePassword(final Handler<Boolean> handler, String query, String password, Map<String, Object> params) { final String pw = BCrypt.hashpw(password, BCrypt.gensalt()); params.put("password", pw); neo.send(query, params, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> res) { JsonObject r = res.body().getJsonObject("result"); handler.handle("ok".equals(res.body().getString("status")) && r.getJsonObject("0") != null && pw.equals(r.getJsonObject("0").getString("pw"))); } }); } }