From 61b12bdebd5fc786abc58b0187433726b53bc749 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 6 May 2019 18:12:44 +0200 Subject: [PATCH] Closes #309: store onboarding status server side --- .../com/sismics/docs/core/dao/UserDao.java | 20 +++++ .../com/sismics/docs/core/model/jpa/User.java | 17 +++- .../src/main/resources/config.properties | 2 +- .../resources/db/update/dbupdate-023-0.sql | 2 + docs-web/src/dev/resources/config.properties | 2 +- .../docs/rest/resource/UserResource.java | 40 ++++++++- .../controller/document/DocumentDefault.js | 89 ++++++++++--------- docs-web/src/prod/resources/config.properties | 2 +- .../src/stress/resources/config.properties | 2 +- .../sismics/docs/rest/TestUserResource.java | 16 +++- 10 files changed, 140 insertions(+), 52 deletions(-) create mode 100644 docs-core/src/main/resources/db/update/dbupdate-023-0.sql diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/UserDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/UserDao.java index c83511b7..2b10f59c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/UserDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/UserDao.java @@ -171,6 +171,26 @@ public class UserDao { return user; } + /** + * Update the onboarding status. + * + * @param user User to update + * @return Updated user + */ + public User updateOnboarding(User user) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the user + Query q = em.createQuery("select u from User u where u.id = :id and u.deleteDate is null"); + q.setParameter("id", user.getId()); + User userDb = (User) q.getSingleResult(); + + // Update the user + userDb.setOnboarding(user.isOnboarding()); + + return user; + } + /** * Gets a user by its ID. * diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java index 30e96274..a7a7c382 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java @@ -46,7 +46,13 @@ public class User implements Loggable { */ @Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100) private String privateKey; - + + /** + * False when the user passed the onboarding. + */ + @Column(name = "USE_ONBOARDING_B", nullable = false) + private boolean onboarding; + /** * TOTP secret key. */ @@ -198,6 +204,15 @@ public class User implements Loggable { return this; } + public boolean isOnboarding() { + return onboarding; + } + + public User setOnboarding(boolean onboarding) { + this.onboarding = onboarding; + return this; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index 5d8d3532..0ac161b7 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=22 \ No newline at end of file +db.version=23 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-023-0.sql b/docs-core/src/main/resources/db/update/dbupdate-023-0.sql new file mode 100644 index 00000000..befc5826 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-023-0.sql @@ -0,0 +1,2 @@ +alter table T_USER add column USE_ONBOARDING_B bit not null default 1; +update T_CONFIG set CFG_VALUE_C = '23' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 7ab7347c..8e983362 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=22 \ No newline at end of file +db.version=23 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index d2f343a3..dd413cac 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -101,6 +101,7 @@ public class UserResource extends BaseResource { user.setPassword(password); user.setEmail(email); user.setStorageQuota(storageQuota); + user.setOnboarding(true); // Create the user UserDao userDao = new UserDao(); @@ -622,6 +623,7 @@ public class UserResource extends BaseResource { * @apiGroup User * @apiSuccess {Boolean} anonymous True if no user is connected * @apiSuccess {Boolean} is_default_password True if the admin has the default password + * @apiSuccess {Boolean} onboarding True if the UI needs to display the onboarding * @apiSuccess {String} username Username * @apiSuccess {String} email E-mail * @apiSuccess {Number} storage_quota Storage quota (in bytes) @@ -665,8 +667,9 @@ public class UserResource extends BaseResource { .add("email", user.getEmail()) .add("storage_quota", user.getStorageQuota()) .add("storage_current", user.getStorageCurrent()) - .add("totp_enabled", user.getTotpKey() != null); - + .add("totp_enabled", user.getTotpKey() != null) + .add("onboarding", user.isOnboarding()); + // Base functions JsonArrayBuilder baseFunctions = Json.createArrayBuilder(); for (String baseFunction : ((UserPrincipal) principal).getBaseFunctionSet()) { @@ -898,6 +901,39 @@ public class UserResource extends BaseResource { .add("status", "ok"); return Response.ok().entity(response.build()).build(); } + + /** + * Mark the onboarding experience as passed. + * + * @api {post} /user/onboarded Mark the onboarding experience as passed + * @apiDescription Once the onboarding experience has been passed by the user, this resource prevent it from being displayed again. + * @apiName PostUserOnboarded + * @apiGroup User + * @apiSuccess {String} status Status OK + * @apiError (client) ForbiddenError Access denied + * @apiPermission user + * @apiVersion 1.7.0 + * + * @return Response + */ + @POST + @Path("onboarded") + public Response onboarded() { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Save it + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(principal.getName()); + user.setOnboarding(false); + userDao.updateOnboarding(user); + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } /** * Enable time-based one-time password. diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js index dfae0ff7..0e158675 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js @@ -3,7 +3,7 @@ /** * Document default controller. */ -angular.module('docs').controller('DocumentDefault', function ($scope, $rootScope, $state, Restangular, Upload, $translate, $uibModal, $dialog) { +angular.module('docs').controller('DocumentDefault', function ($scope, $rootScope, $state, Restangular, Upload, $translate, $uibModal, $dialog, User) { // Load user audit log Restangular.one('auditlog').get().then(function (data) { $scope.logs = data.logs; @@ -145,48 +145,51 @@ angular.module('docs').controller('DocumentDefault', function ($scope, $rootScop // Onboarding $translate('onboarding.step1.title').then(function () { - if (localStorage.onboardingDisplayed || $(window).width() < 1000) { - return; - } - localStorage.onboardingDisplayed = true; - - $rootScope.onboardingEnabled = true; - - $rootScope.onboardingSteps = [ - { - title: $translate.instant('onboarding.step1.title'), - description: $translate.instant('onboarding.step1.description'), - position: 'centered', - width: 300 - }, - { - title: $translate.instant('onboarding.step2.title'), - description: $translate.instant('onboarding.step2.description'), - attachTo: '#document-add-btn', - position: 'right', - width: 300 - }, - { - title: $translate.instant('onboarding.step3.title'), - description: $translate.instant('onboarding.step3.description'), - attachTo: '#quick-upload-zone', - position: 'left', - width: 300 - }, - { - title: $translate.instant('onboarding.step4.title'), - description: $translate.instant('onboarding.step4.description'), - attachTo: '#search-box', - position: 'right', - width: 300 - }, - { - title: $translate.instant('onboarding.step5.title'), - description: $translate.instant('onboarding.step5.description'), - attachTo: '#navigation-tag', - position: "right", - width: 300 + User.userInfo().then(function(userData) { + if (!userData.onboarding || $(window).width() < 1000) { + return; } - ]; + Restangular.one('user').post('onboarded'); + $rootScope.userInfo.onboarding = false; + + $rootScope.onboardingEnabled = true; + + $rootScope.onboardingSteps = [ + { + title: $translate.instant('onboarding.step1.title'), + description: $translate.instant('onboarding.step1.description'), + position: 'centered', + width: 300 + }, + { + title: $translate.instant('onboarding.step2.title'), + description: $translate.instant('onboarding.step2.description'), + attachTo: '#document-add-btn', + position: 'right', + width: 300 + }, + { + title: $translate.instant('onboarding.step3.title'), + description: $translate.instant('onboarding.step3.description'), + attachTo: '#quick-upload-zone', + position: 'left', + width: 300 + }, + { + title: $translate.instant('onboarding.step4.title'), + description: $translate.instant('onboarding.step4.description'), + attachTo: '#search-box', + position: 'right', + width: 300 + }, + { + title: $translate.instant('onboarding.step5.title'), + description: $translate.instant('onboarding.step5.description'), + attachTo: '#navigation-tag', + position: "right", + width: 300 + } + ]; + }); }); }); \ No newline at end of file diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 7ab7347c..8e983362 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=22 \ No newline at end of file +db.version=23 \ No newline at end of file diff --git a/docs-web/src/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties index 7ab7347c..8e983362 100644 --- a/docs-web/src/stress/resources/config.properties +++ b/docs-web/src/stress/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=22 \ No newline at end of file +db.version=23 \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index a727ff91..2aeab9fa 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -172,7 +172,7 @@ public class TestUserResource extends BaseJerseyTest { .get(); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); json = response.readEntity(JsonObject.class); - Assert.assertEquals(true, json.getBoolean("anonymous")); + Assert.assertTrue(json.getBoolean("anonymous")); // Check alice user information json = target().path("/user").request() @@ -187,8 +187,20 @@ public class TestUserResource extends BaseJerseyTest { json = target().path("/user").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, bobToken) .get(JsonObject.class); + Assert.assertTrue(json.getBoolean("onboarding")); Assert.assertEquals("bob@docs.com", json.getString("email")); - + + // Pass onboarding + target().path("/user/onboarded").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, bobToken) + .post(Entity.form(new Form()), JsonObject.class); + + // Check bob user information + json = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, bobToken) + .get(JsonObject.class); + Assert.assertFalse(json.getBoolean("onboarding")); + // Test login KO (user not found) response = target().path("/user/login").request() .post(Entity.form(new Form()