system configuration page: read-only mode, title, homepage message

This commit is contained in:
Michele Artini 2021-09-28 16:05:21 +02:00
parent 92ae3fb533
commit 654d3fc397
13 changed files with 234 additions and 20 deletions

View File

@ -17,14 +17,16 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import eu.dnetlib.common.controller.AbstractDnetController; import eu.dnetlib.common.controller.AbstractDnetController;
import eu.dnetlib.organizations.model.SystemConfiguration;
import eu.dnetlib.organizations.model.utils.VocabularyTerm; import eu.dnetlib.organizations.model.utils.VocabularyTerm;
import eu.dnetlib.organizations.model.view.UserView; import eu.dnetlib.organizations.model.view.UserView;
import eu.dnetlib.organizations.repository.SystemConfigurationRepository;
import eu.dnetlib.organizations.repository.UserRepository; import eu.dnetlib.organizations.repository.UserRepository;
import eu.dnetlib.organizations.repository.readonly.UserViewRepository; import eu.dnetlib.organizations.repository.readonly.UserViewRepository;
import eu.dnetlib.organizations.utils.DatabaseUtils; import eu.dnetlib.organizations.utils.DatabaseUtils;
@RestController @RestController
public class UserController extends AbstractDnetController { public class AdminController extends AbstractDnetController {
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@ -32,6 +34,9 @@ public class UserController extends AbstractDnetController {
@Autowired @Autowired
private UserViewRepository userViewRepository; private UserViewRepository userViewRepository;
@Autowired
private SystemConfigurationRepository systemConfigurationRepository;
@Autowired @Autowired
private DatabaseUtils dbUtils; private DatabaseUtils dbUtils;
@ -76,17 +81,35 @@ public class UserController extends AbstractDnetController {
} }
@PostMapping("/api/users") @PostMapping("/api/users")
public Iterable<UserView> save(@RequestBody final UserView userView, final Authentication authentication) { public Iterable<UserView> saveUser(@RequestBody final UserView userView, final Authentication authentication) {
if (UserInfo.getEmail(authentication).equals(userView.getEmail())) { throw new RuntimeException("You can't edit your own user"); } if (UserInfo.getEmail(authentication).equals(userView.getEmail())) { throw new RuntimeException("You can't edit your own user"); }
dbUtils.saveUser(userView); dbUtils.saveUser(userView);
return users(authentication); return users(authentication);
} }
@DeleteMapping("/api/users") @DeleteMapping("/api/users")
public Iterable<UserView> delete(final @RequestParam String email, final Authentication authentication) { public Iterable<UserView> deleteUser(final @RequestParam String email, final Authentication authentication) {
if (UserInfo.getEmail(authentication).equals(email)) { throw new RuntimeException("You can't delete your own user"); } if (UserInfo.getEmail(authentication).equals(email)) { throw new RuntimeException("You can't delete your own user"); }
dbUtils.deleteUser(email); dbUtils.deleteUser(email);
return users(authentication); return users(authentication);
} }
@GetMapping("/api/sysconf")
public SystemConfiguration sysConf(final Authentication authentication) {
if (UserInfo.isSuperAdmin(authentication)) {
return systemConfigurationRepository.findById(SystemConfiguration.DEFAULT_ID).get();
} else {
throw new RuntimeException("User not authorized");
}
}
@PostMapping("/api/sysconf")
public SystemConfiguration saveSysConf(@RequestBody final SystemConfiguration sysConf, final Authentication authentication) {
if (UserInfo.isSuperAdmin(authentication)) {
sysConf.setId(SystemConfiguration.DEFAULT_ID);
return systemConfigurationRepository.save(sysConf);
} else {
throw new RuntimeException("User not authorized");
}
}
} }

View File

@ -9,6 +9,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import eu.dnetlib.common.controller.AbstractDnetController; import eu.dnetlib.common.controller.AbstractDnetController;
import eu.dnetlib.organizations.model.SystemConfiguration;
import eu.dnetlib.organizations.repository.SystemConfigurationRepository;
@Controller @Controller
public class HomeController extends AbstractDnetController { public class HomeController extends AbstractDnetController {
@ -16,6 +18,9 @@ public class HomeController extends AbstractDnetController {
@Autowired @Autowired
private Environment env; private Environment env;
@Autowired
private SystemConfigurationRepository systemConfigurationRepository;
@GetMapping("/") @GetMapping("/")
public String home() { public String home() {
return env.acceptsProfiles(Profiles.of("dev")) ? "redirect:main" : "home"; return env.acceptsProfiles(Profiles.of("dev")) ? "redirect:main" : "home";
@ -46,6 +51,11 @@ public class HomeController extends AbstractDnetController {
return authentication != null ? UserInfo.getEmail(authentication) : null; return authentication != null ? UserInfo.getEmail(authentication) : null;
} }
@ModelAttribute("sysconf")
public SystemConfiguration getSysConf(final Authentication authentication) {
return systemConfigurationRepository.findById(SystemConfiguration.DEFAULT_ID).get();
}
@GetMapping({ @GetMapping({
"/doc", "/swagger" "/doc", "/swagger"
}) })

View File

@ -0,0 +1,75 @@
package eu.dnetlib.organizations.model;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "sysconf")
public class SystemConfiguration implements Serializable {
/**
*
*/
private static final long serialVersionUID = -7935575713678578023L;
public static final String DEFAULT_ID = "default";
@Id
@Column(name = "id")
private String id;
@Column(name = "title")
private String title;
@Column(name = "homepage_msg")
private String homepageMessage;
@Column(name = "readonly")
private Boolean readonly;
public SystemConfiguration() {}
public SystemConfiguration(final String id, final String title, final String homepageMessage, final Boolean readonly) {
this.id = id;
this.title = title;
this.homepageMessage = homepageMessage;
this.readonly = readonly;
}
public String getId() {
return id;
}
public void setId(final String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(final String title) {
this.title = title;
}
public String getHomepageMessage() {
return homepageMessage;
}
public void setHomepageMessage(final String homepageMessage) {
this.homepageMessage = homepageMessage;
}
public Boolean getReadonly() {
return readonly;
}
public void setReadonly(final Boolean readonly) {
this.readonly = readonly;
}
}

View File

@ -0,0 +1,9 @@
package eu.dnetlib.organizations.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import eu.dnetlib.organizations.model.SystemConfiguration;
public interface SystemConfigurationRepository extends JpaRepository<SystemConfiguration, String> {
}

View File

@ -6,6 +6,7 @@ DROP VIEW IF EXISTS conflict_groups_view;
DROP VIEW IF EXISTS suggestions_info_by_country_view; DROP VIEW IF EXISTS suggestions_info_by_country_view;
DROP VIEW IF EXISTS duplicate_groups_view; DROP VIEW IF EXISTS duplicate_groups_view;
DROP TABLE IF EXISTS sysconf;
DROP TABLE IF EXISTS other_ids; DROP TABLE IF EXISTS other_ids;
DROP TABLE IF EXISTS other_names; DROP TABLE IF EXISTS other_names;
DROP TABLE IF EXISTS acronyms; DROP TABLE IF EXISTS acronyms;
@ -26,6 +27,14 @@ DROP TABLE IF EXISTS languages;
DROP SEQUENCE IF EXISTS organizations_id_seq; DROP SEQUENCE IF EXISTS organizations_id_seq;
CREATE TABLE sysconf (
id text PRIMARY KEY DEFAULT 'default',
title text NOT NULL DEFAULT 'OpenOrgs Database',
homepage_msg text NOT NULL DEFAULT '',
readonly boolean NOT NULL DEFAULT false
);
INSERT INTO sysconf(id) VALUES ('default');
CREATE TABLE org_types (val text PRIMARY KEY, name text); CREATE TABLE org_types (val text PRIMARY KEY, name text);
INSERT INTO org_types(val) VALUES ('Archive'), ('Company'), ('Education'), ('Facility'), ('Government'), ('Healthcare'), ('Nonprofit'), ('Other'), ('UNKNOWN'); INSERT INTO org_types(val) VALUES ('Archive'), ('Company'), ('Education'), ('Facility'), ('Government'), ('Healthcare'), ('Nonprofit'), ('Other'), ('UNKNOWN');
UPDATE org_types SET name = val; UPDATE org_types SET name = val;

View File

@ -0,0 +1,32 @@
<h4>System configuration</h4>
<br />
<div class="row">
<div class="col-sm-12 col-lg-10">
<form>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Title</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="sysconf.title" placeholder="title here" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Homepage message</label>
<div class="col-sm-10">
<textarea class="form-control" ng-model="sysconf.homepageMessage" placeholder="html message here" rows="5"></textarea>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Read only</label>
<div class="col-sm-10">
<input class="form-control" type="checkbox" ng-model="sysconf.readonly" />
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-click="saveConf()">Save</button>
</div>
</div>
</form>
</div>
</div>

View File

@ -95,7 +95,7 @@
<div class="card mb-3" ng-if="currentUser.valid && ((currentUser.role == 'USER') || (currentUser.role == 'NATIONAL_ADMIN'))"> <div class="card mb-3" ng-if="currentUser.valid && ((currentUser.role == 'USER') || (currentUser.role == 'NATIONAL_ADMIN'))">
<div class="card-header bg-primary text-white py-1">Countries</div> <div class="card-header bg-primary text-white py-1">Countries</div>
<div class="card-body"> <div class="card-body">
<input type="text" class="form-control form-control-sm mb-3" ng-model="countryFilter" placeholder="Filter country..."> <input type="text" class="form-control form-control-sm mb-3" ng-model="countryFilter" ng-show="vocabularies.countries.length > 10" placeholder="Filter country...">
<div class="form-group row"> <div class="form-group row">
<div class="col-xs-12 col-md-6 col-lg-4" ng-repeat="c in vocabularies.countries | filter:countryFilter"> <div class="col-xs-12 col-md-6 col-lg-4" ng-repeat="c in vocabularies.countries | filter:countryFilter">
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">

View File

@ -380,6 +380,7 @@ orgsModule.config(function($routeProvider) {
.when('/duplicates/:country', { templateUrl: 'resources/html/pages/advanced/duplicates.html', controller: 'duplicatesCtrl' }) .when('/duplicates/:country', { templateUrl: 'resources/html/pages/advanced/duplicates.html', controller: 'duplicatesCtrl' })
.when('/conflicts/:country', { templateUrl: 'resources/html/pages/advanced/conflicts.html', controller: 'conflictsCtrl' }) .when('/conflicts/:country', { templateUrl: 'resources/html/pages/advanced/conflicts.html', controller: 'conflictsCtrl' })
.when('/users', { templateUrl: 'resources/html/pages/admin/users.html', controller: 'usersCtrl' }) .when('/users', { templateUrl: 'resources/html/pages/admin/users.html', controller: 'usersCtrl' })
.when('/sysconf', { templateUrl: 'resources/html/pages/admin/sysConf.html', controller: 'sysConfCtrl' })
.otherwise({ redirectTo: '/search' }); .otherwise({ redirectTo: '/search' });
}); });
@ -778,7 +779,21 @@ orgsModule.controller('conflictsCtrl', function ($scope, $http, $routeParams, $l
$scope.refresh(); $scope.refresh();
}); });
orgsModule.controller('sysConfCtrl', function ($scope, $http, $timeout, $route) {
$scope.sysconf = {};
call_http_get($http, 'api/sysconf', function(res) { $scope.sysconf = res.data; });
$scope.saveConf = function() {
// I use force_http_post because call_http_post could be disabled if the system is in readonly mode
force_http_post($http, 'api/sysconf', $scope.sysconf, function(res) {
$scope.sysconf = res.data;
alert("Configuration saved !");
window.location.reload();
});
}
});
orgsModule.controller('usersCtrl', function ($scope, $http, $timeout, $route, vocabulariesService) { orgsModule.controller('usersCtrl', function ($scope, $http, $timeout, $route, vocabulariesService) {
$scope.users = []; $scope.users = [];

View File

@ -13,7 +13,7 @@
<!-- Icons CSS --> <!-- Icons CSS -->
<link rel="stylesheet" href="common/css/fontawesome-all.min.css"> <link rel="stylesheet" href="common/css/fontawesome-all.min.css">
<title>Organizations Database: user already registered</title> <title th:text="${sysconf.title} + ': user already registered'"></title>
</head> </head>
<body> <body>
@ -21,7 +21,7 @@
<div class="container"> <div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#"> <a class="navbar-brand" href="#">
<img src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenOrgs Database"> OpenOrgs Database <img src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenAIRE logo"> <span th:text="${sysconf.title}"></span>
</a> </a>
</nav> </nav>

View File

@ -13,7 +13,7 @@
<!-- Icons CSS --> <!-- Icons CSS -->
<link rel="stylesheet" href="common/css/fontawesome-all.min.css"> <link rel="stylesheet" href="common/css/fontawesome-all.min.css">
<title>Organizations Database: authorization request</title> <title th:text="${sysconf.title} + ': authorization request'"></title>
</head> </head>
<body ng-app="authReqApp" ng-controller="authReqCtrl"> <body ng-app="authReqApp" ng-controller="authReqCtrl">
@ -21,8 +21,7 @@
<div class="container"> <div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#"> <a class="navbar-brand" href="#">
<img src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenOrgs Database"> OpenOrgs Database <img src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenAIRE logo"> <span th:text="${sysconf.title}"></span> </a>
</a>
</nav> </nav>
<div ng-if="registrationMessage"> <div ng-if="registrationMessage">

View File

@ -15,7 +15,7 @@
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link rel="stylesheet" href="common/css/bootstrap.cerulean.min.css" /> <link rel="stylesheet" href="common/css/bootstrap.cerulean.min.css" />
<title>Organizations Database: Login</title> <title th:text="${sysconf.title} + ': login'"></title>
</head> </head>
<body> <body>
@ -23,8 +23,7 @@
<div> <div>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#"> <a class="navbar-brand" href="#">
<img src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenOrgs Database"> OpenOrgs Database <img src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenAIRE logo"> <span th:text="${sysconf.title}"></span> </a>
</a>
</nav> </nav>
</div> </div>
@ -41,6 +40,10 @@
<div class="card-body"> <div class="card-body">
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active text-center p-5" id="tabLogin"> <div class="tab-pane active text-center p-5" id="tabLogin">
<div th:utext="${sysconf.homepageMessage}"></div>
<hr th:if="${sysconf.homepageMessage != '' and sysconf.homepageMessage != null}" />
<a href="./main" class="btn btn-lg btn-primary">Access using OpenAIRE credentials</a> <a href="./main" class="btn btn-lg btn-primary">Access using OpenAIRE credentials</a>
</div> </div>
<div class="tab-pane" id="tabRegister"> <div class="tab-pane" id="tabRegister">

View File

@ -15,7 +15,7 @@
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link rel="stylesheet" href="common/css/bootstrap.cerulean.min.css" /> <link rel="stylesheet" href="common/css/bootstrap.cerulean.min.css" />
<title>Organizations Database: Login</title> <title th:text="${sysconf.title} + ': login'"></title>
</head> </head>
<body> <body>
@ -23,8 +23,7 @@
<div> <div>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#"> <a class="navbar-brand" href="#">
<img src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenOrgs Database"> OpenOrgs Database <img src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenAIRE logo"> <span th:text="${sysconf.title}"></span> </a>
</a>
</nav> </nav>
</div> </div>
@ -40,6 +39,10 @@
<div class="card-body"> <div class="card-body">
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="tabLogin"> <div class="tab-pane active" id="tabLogin">
<div th:utext="${sysconf.homepageMessage}"></div>
<hr th:if="${sysconf.homepageMessage != '' and sysconf.homepageMessage != null}" />
<form th:action="@{/login}" method="post"> <form th:action="@{/login}" method="post">
<div th:if="${param.error}"> <div th:if="${param.error}">
<div class="alert alert-danger">Invalid username and password.</div> <div class="alert alert-danger">Invalid username and password.</div>

View File

@ -45,7 +45,7 @@ fieldset > legend { font-size : 1.2rem !important; }
</style> </style>
<title>Organizations Database</title> <title th:text="${sysconf.title}"></title>
</head> </head>
@ -57,8 +57,8 @@ fieldset > legend { font-size : 1.2rem !important; }
<nav class="navbar navbar-expand-lg navbar-dark bg-primary" ng-controller="menuCtrl"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary" ng-controller="menuCtrl">
<a class="navbar-brand" href="#"> <img <a class="navbar-brand" href="#"> <img
src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenOrgs Database"> src="resources/images/openaire_logo_small.png" width="30" height="30" alt="OpenAIRE logo">
OpenOrgs Database <span th:text="${sysconf.title}"></span>
</a> </a>
<button class="navbar-toggler" type="button" data-toggle="collapse" <button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarSupportedContent"> data-target="#navbarSupportedContent">
@ -109,6 +109,8 @@ fieldset > legend { font-size : 1.2rem !important; }
<li class="nav-item dropdown" sec:authorize="hasRole('ROLE_OPENORGS_ADMIN') or hasRole('ROLE_OPENORGS_NATIONAL_ADMIN')"> <li class="nav-item dropdown" sec:authorize="hasRole('ROLE_OPENORGS_ADMIN') or hasRole('ROLE_OPENORGS_NATIONAL_ADMIN')">
<a class="nav-link dropdown-toggle" href="javascript:void(0)" data-toggle="dropdown"><i class="fa fa-cog"></i></a> <a class="nav-link dropdown-toggle" href="javascript:void(0)" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#!/sysconf" sec:authorize="hasRole('ROLE_OPENORGS_ADMIN')">System configuration</a>
<div class="dropdown-divider" sec:authorize="hasRole('ROLE_OPENORGS_ADMIN')"></div>
<a class="dropdown-item" href="#!/users">Manage users</a> <a class="dropdown-item" href="#!/users">Manage users</a>
</div> </div>
</li> </li>
@ -137,6 +139,9 @@ fieldset > legend { font-size : 1.2rem !important; }
</div> </div>
</nav> </nav>
<div class="alert alert-danger text-center" th:if="${sysconf.readonly}">The portal is running in READ-ONLY mode.</div>
<div class="container-fluid small mt-4" ng-view></div> <div class="container-fluid small mt-4" ng-view></div>
@ -192,7 +197,7 @@ fieldset > legend { font-size : 1.2rem !important; }
spinnerTarget.style.visibility = 'hidden'; spinnerTarget.style.visibility = 'hidden';
spinner.stop(); spinner.stop();
} }
function call_http_get($http, url, onSuccess) { function call_http_get($http, url, onSuccess) {
showSpinner(); showSpinner();
@ -208,7 +213,38 @@ fieldset > legend { font-size : 1.2rem !important; }
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')'); alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
}); });
} }
function force_http_post($http, url, obj, onSuccess) {
showSpinner();
$http.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
$http.post(url, obj).then(function successCallback(res) {
hideSpinner();
if ((typeof res.data) == 'string') {
alert("Session expired !"); location.reload(true);
} else {
onSuccess(res);
}
}, function errorCallback(res) {
hideSpinner();
alert('ERROR: ' + res.data.error + ' (' + res.data.message + ')');
});
}
</script>
<script th:if="${sysconf.readonly}">
function call_http_post($http, url, obj, onSuccess) {
alert("Method not permitted in READ-ONLY mode !")
}
function call_http_delete($http, url, onSuccess) {
alert("Method not permitted in READ-ONLY mode !")
}
</script>
<script th:if="${!sysconf.readonly}">
function call_http_post($http, url, obj, onSuccess) { function call_http_post($http, url, obj, onSuccess) {
showSpinner(); showSpinner();
$http.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8"; $http.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";