diff --git a/grails-app/assets/javascripts/application.js b/grails-app/assets/javascripts/application.js index daaee80c7..6f61e9e74 100644 --- a/grails-app/assets/javascripts/application.js +++ b/grails-app/assets/javascripts/application.js @@ -2,11 +2,11 @@ // // Any JavaScript file within this directory can be referenced here using a relative path. // -// You're free to add application-wide JavaScript to this file, but it's generally better +// You're free to add application-wide JavaScript to this file, but it's generally better // to create separate JavaScript files as needed. // -//= require jquery //= require spring-websocket +//= require jquery/dist/jquery.js //= require angular/angular.js //= require angular-ui-router/release/angular-ui-router.js //= require angular-sanitize/angular-sanitize.js @@ -18,6 +18,7 @@ //= require jquery-ui-1.11.4.custom/jquery-ui.js //= require angular-ui-slider/src/slider.js //= require mousetrap/mousetrap.js +//= require Autolinker.js/dist/Autolinker.js //= require streama-app.js //= require_tree . -//= require_self \ No newline at end of file +//= require_self diff --git a/grails-app/assets/javascripts/controllers/admin-settings-ctrl.js b/grails-app/assets/javascripts/controllers/admin-settings-ctrl.js new file mode 100644 index 000000000..f9ead77dd --- /dev/null +++ b/grails-app/assets/javascripts/controllers/admin-settings-ctrl.js @@ -0,0 +1,59 @@ +'use strict'; + +streamaApp.controller('adminSettingsCtrl', ['$scope', 'apiService', '$sce', function ($scope, apiService, $sce) { + + $scope.loading = true; + + apiService.settings.list().success(function (data) { + $scope.settings = data; + + _.forEach(data, function (setting) { + setting.description = $sce.trustAsHtml(Autolinker.link(setting.description, { newWindow: "true" } )); + }); + $scope.loading = false; + }); + + $scope.updateMultipleSettings = function (settings) { + settings.invalid = false; + + apiService.settings.updateMultiple(settings) + .success(function () { + window.location.reload(); + }) + .error(function () { + alertify.error('There was an error saving your settings. Please refer to the server-log.'); + }); + }; + + + $scope.validateSettings = function (settings) { + $scope.changeValue(settings); + $scope.loading = true; + + apiService.settings.validateSettings(settings) + .success(function (data) { + alertify.success(data.message); + settings.valid = true; + $scope.loading = false; + }) + .error(function (data) { + alertify.error(data.message); + settings.invalid = true; + $scope.loading = false; + }); + }; + + $scope.changeValue = function (settings) { + settings.valid = undefined; + settings.invalid = undefined; + settings.dirty = settings.value; + }; + + + $scope.anySettingsInvalid = function () { + return _.find($scope.settings, function (setting) { + return setting.invalid || (setting.dirty && !setting.valid) || !setting.value; + }); + }; + +}]); diff --git a/grails-app/assets/javascripts/controllers/dash-ctrl.js b/grails-app/assets/javascripts/controllers/dash-ctrl.js index e63e1e7f1..bd42e036b 100644 --- a/grails-app/assets/javascripts/controllers/dash-ctrl.js +++ b/grails-app/assets/javascripts/controllers/dash-ctrl.js @@ -1,18 +1,28 @@ 'use strict'; -streamaApp.controller('dashCtrl', ['$scope', 'apiService', function ($scope, apiService) { +streamaApp.controller('dashCtrl', ['$scope', 'apiService', '$state', function ($scope, apiService, $state) { $scope.loading = true; - - apiService.video.dash() - .success(function (data) { - $scope.episodes = data.firstEpisodes; - $scope.continueWatching = data.continueWatching; - $scope.movies = data.movies; - $scope.loading = false; - }) - .error(function () { - alertify('A server error occured.'); - $scope.loading = false; - }); -}]); \ No newline at end of file + apiService.settings.list().success(function (data) { + var TheMovieDbAPI = _.find(data, {settingsKey: 'TheMovieDB API key'}); + + if(!TheMovieDbAPI.value){ + alertify.alert('You need to fill out some required base-settings. You will be redirected to the settings page now.', function () { + $state.go('admin.settings'); + }); + }else{ + apiService.video.dash() + .success(function (data) { + $scope.episodes = data.firstEpisodes; + $scope.continueWatching = data.continueWatching; + $scope.movies = data.movies; + $scope.loading = false; + }) + .error(function () { + alertify('A server error occured.'); + $scope.loading = false; + }); + } + }); + +}]); diff --git a/grails-app/assets/javascripts/services/api-service.js b/grails-app/assets/javascripts/services/api-service.js index f2720ab8f..3c36f3883 100644 --- a/grails-app/assets/javascripts/services/api-service.js +++ b/grails-app/assets/javascripts/services/api-service.js @@ -7,8 +7,8 @@ streamaApp.factory('apiService', ['$http', function ($http) { currentUser: function () { return $http.get(urlBase + 'user/current.json'); }, - - + + tvShow: { get: function (id) { return $http.get(urlBase + 'tvShow/show.json', {params: {id: id}}); @@ -89,7 +89,7 @@ streamaApp.factory('apiService', ['$http', function ($http) { return $http.get(urlBase + 'episode.json', {params: params}); } }, - + movie: { get: function (id) { return $http.get(urlBase + 'movie/show.json', {params: {id: id}}); @@ -115,6 +115,19 @@ streamaApp.factory('apiService', ['$http', function ($http) { }, + settings: { + list: function () { + return $http.get(urlBase + 'settings.json'); + }, + updateMultiple: function (data) { + return $http.post(urlBase + 'settings/updateMultiple.json', data); + }, + validateSettings: function (data) { + return $http.post(urlBase + 'settings/validateSettings.json', data); + } + }, + + theMovieDb: { search: function (type, name) { return $http.get(urlBase + 'theMovieDb/search.json', {params: {type: type, name: name}}); @@ -123,11 +136,11 @@ streamaApp.factory('apiService', ['$http', function ($http) { return $http.get(urlBase + 'theMovieDb/seasonForShow.json', {params: params}); } }, - + websocket: { triggerPlayerAction: function (params) { return $http.get(urlBase + 'websocket/triggerPlayerAction.json', {params: params}); } } }; -}]); \ No newline at end of file +}]); diff --git a/grails-app/assets/javascripts/services/upload-service.js b/grails-app/assets/javascripts/services/upload-service.js index 020fa4cf8..5909689a8 100644 --- a/grails-app/assets/javascripts/services/upload-service.js +++ b/grails-app/assets/javascripts/services/upload-service.js @@ -21,10 +21,14 @@ streamaApp.factory('uploadService', ['$http', 'Upload', '$location', function ($ uploadStatus.percentage = progressPercentage; }) - .success(callback || angular.noop); + .success(callback || angular.noop) + .error(function (err) { + console.log('%c err', 'color: deeppink; font-weight: bold; text-shadow: 0 0 5px deeppink;', arguments); + alertify.error(err) + }); } } } }; -}]); \ No newline at end of file +}]); diff --git a/grails-app/assets/javascripts/streama-app.js b/grails-app/assets/javascripts/streama-app.js index eca640f7a..75f6f4b79 100644 --- a/grails-app/assets/javascripts/streama-app.js +++ b/grails-app/assets/javascripts/streama-app.js @@ -39,6 +39,11 @@ streamaApp.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', func templateUrl: 'admin-users.htm', controller: 'adminUsersCtrl' }) + .state('admin.settings', { + url: '/settings', + templateUrl: 'admin-settings.htm', + controller: 'adminSettingsCtrl' + }) .state('admin.shows', { url: '/shows', templateUrl: 'admin-shows.htm', @@ -70,6 +75,11 @@ streamaApp.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', func return response || $q.when(response); }, responseError: function (response) { + + if(response.status != 404){ + alertify.error('A system error occurred'); + } + return $q.reject(response); } }; @@ -80,4 +90,4 @@ streamaApp.run(['$rootScope', '$state', function ($rootScope, $state) { $rootScope.isCurrentState = function (stateName) { return ($state.current.name == stateName); }; -}]); \ No newline at end of file +}]); diff --git a/grails-app/assets/javascripts/streama-app/templates/admin-settings.tpl.htm b/grails-app/assets/javascripts/streama-app/templates/admin-settings.tpl.htm new file mode 100644 index 000000000..94aef44df --- /dev/null +++ b/grails-app/assets/javascripts/streama-app/templates/admin-settings.tpl.htm @@ -0,0 +1,40 @@ +

+ Settings + +
+
+
+
+
+

+ + + +
+ +
+
+
+ +
+
+ + + + + +
+
+ +
+
+

+
+
+
+ + +
+ + + diff --git a/grails-app/assets/javascripts/streama-app/templates/admin.tpl.htm b/grails-app/assets/javascripts/streama-app/templates/admin.tpl.htm index 7b7172706..cd0e9574c 100644 --- a/grails-app/assets/javascripts/streama-app/templates/admin.tpl.htm +++ b/grails-app/assets/javascripts/streama-app/templates/admin.tpl.htm @@ -10,10 +10,13 @@
  • Users
  • +
  • + Settings +
  • - +
    - \ No newline at end of file + diff --git a/grails-app/assets/stylesheets/_settings.scss b/grails-app/assets/stylesheets/_settings.scss new file mode 100644 index 000000000..62770a7ad --- /dev/null +++ b/grails-app/assets/stylesheets/_settings.scss @@ -0,0 +1,9 @@ +.settings-form{ + @include clearfix; + + .settings-description{ + margin-top: 7px; + opacity: 0.8; + margin-bottom: 30px; + } +} diff --git a/grails-app/assets/stylesheets/style.css b/grails-app/assets/stylesheets/style.css index bfdd12029..8a26450bd 100644 --- a/grails-app/assets/stylesheets/style.css +++ b/grails-app/assets/stylesheets/style.css @@ -4245,3 +4245,12 @@ body.mdx-active #mdx-control-view { .modal-dialog .modal-footer { border-color: rgba(255, 255, 255, 0.25); padding: 9px 14px; } + +.settings-form:after { + content: ""; + display: table; + clear: both; } +.settings-form .settings-description { + margin-top: 7px; + opacity: 0.8; + margin-bottom: 30px; } diff --git a/grails-app/assets/stylesheets/style.scss b/grails-app/assets/stylesheets/style.scss index 8ef6fc11b..68b3e7266 100644 --- a/grails-app/assets/stylesheets/style.scss +++ b/grails-app/assets/stylesheets/style.scss @@ -15,3 +15,4 @@ @import "player-controls"; @import "invite"; @import "modal"; +@import "settings"; diff --git a/grails-app/conf/BootStrap.groovy b/grails-app/conf/BootStrap.groovy index f80bf815b..9072b6559 100644 --- a/grails-app/conf/BootStrap.groovy +++ b/grails-app/conf/BootStrap.groovy @@ -1,5 +1,5 @@ class BootStrap { - + def marshallerService def defaultDataService @@ -7,6 +7,7 @@ class BootStrap { marshallerService.init() defaultDataService.createDefaultRoles() defaultDataService.createDefaultUsers() + defaultDataService.createDefaultSettings() } def destroy = { } diff --git a/grails-app/conf/Config.groovy b/grails-app/conf/Config.groovy index c52a41fbc..83f4a660f 100644 --- a/grails-app/conf/Config.groovy +++ b/grails-app/conf/Config.groovy @@ -150,12 +150,6 @@ log4j.main = { } -streama { - storage { - path = "/data/streama" - } - themoviedbAPI = "e1584c7cc0072947d4776de6df7b8822" -} grails.databinding.dateFormats = [ "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", // javascript format in json @@ -167,13 +161,12 @@ grails.databinding.dateFormats = [ grails.plugin.springsecurity.userLookup.userDomainClassName = 'streama.User' grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'streama.UserRole' grails.plugin.springsecurity.authority.className = 'streama.Role' -grails.plugin.springsecurity.successHandler.defaultTargetUrl = '/user/loginTarget' grails.plugin.springsecurity.controllerAnnotations.staticRules = [ '/': ['IS_AUTHENTICATED_REMEMBERED'], '/index': ['IS_AUTHENTICATED_REMEMBERED'], '/index.gsp': ['IS_AUTHENTICATED_REMEMBERED'], - + '/tvShow/**': ['IS_AUTHENTICATED_REMEMBERED'], '/video/**': ['IS_AUTHENTICATED_REMEMBERED'], '/viewingStatus/**': ['IS_AUTHENTICATED_REMEMBERED'], @@ -186,6 +179,7 @@ grails.plugin.springsecurity.controllerAnnotations.staticRules = [ '/appSettings/**': ['IS_AUTHENTICATED_REMEMBERED'], '/stomp/**': ['IS_AUTHENTICATED_REMEMBERED'], '/websocket/**': ['IS_AUTHENTICATED_REMEMBERED'], + '/settings/**': ['IS_AUTHENTICATED_REMEMBERED'], '/invite/**': ['permitAll'], '/assets/**': ['permitAll'], diff --git a/grails-app/conf/UrlMappings.groovy b/grails-app/conf/UrlMappings.groovy index 69634b881..74cada367 100644 --- a/grails-app/conf/UrlMappings.groovy +++ b/grails-app/conf/UrlMappings.groovy @@ -8,6 +8,8 @@ class UrlMappings { } "/"(view:"/index") + "/setSettings"(view:'/setSettings') + "500"(view:'/error') } } diff --git a/grails-app/controllers/streama/SettingsController.groovy b/grails-app/controllers/streama/SettingsController.groovy index 168753082..32ceabbe4 100644 --- a/grails-app/controllers/streama/SettingsController.groovy +++ b/grails-app/controllers/streama/SettingsController.groovy @@ -1,64 +1,76 @@ package streama - import static org.springframework.http.HttpStatus.* import grails.transaction.Transactional @Transactional(readOnly = true) class SettingsController { - static responseFormats = ['json', 'xml'] - static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"] + def settingsService + + static responseFormats = ['json', 'xml'] + static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"] - def index(Integer max) { - params.max = Math.min(max ?: 10, 100) - respond Settings.list(params), [status: OK] + def index(Integer max) { + params.max = Math.min(max ?: 10, 100) + respond Settings.list(params), [status: OK] + } + + @Transactional + def save(Settings settingsInstance) { + if (settingsInstance == null) { + render status: NOT_FOUND + return } - @Transactional - def save(Settings settingsInstance) { - if (settingsInstance == null) { - render status: NOT_FOUND - return - } - - settingsInstance.validate() - if (settingsInstance.hasErrors()) { - render status: NOT_ACCEPTABLE - return - } - - settingsInstance.save flush:true - respond settingsInstance, [status: CREATED] + settingsInstance.validate() + if (settingsInstance.hasErrors()) { + render status: NOT_ACCEPTABLE + return } - @Transactional - def update(Settings settingsInstance) { - if (settingsInstance == null) { - render status: NOT_FOUND - return - } - - settingsInstance.validate() - if (settingsInstance.hasErrors()) { - render status: NOT_ACCEPTABLE - return - } - - settingsInstance.save flush:true - respond settingsInstance, [status: OK] + settingsInstance.save flush: true + respond settingsInstance, [status: CREATED] + } + + + @Transactional + def delete(Settings settingsInstance) { + + if (settingsInstance == null) { + render status: NOT_FOUND + return } - @Transactional - def delete(Settings settingsInstance) { + settingsInstance.delete flush: true + render status: NO_CONTENT + } + - if (settingsInstance == null) { - render status: NOT_FOUND - return - } + def validateSettings(Settings settingsInstance) { + def resultValue = settingsService.validate(settingsInstance) - settingsInstance.delete flush:true - render status: NO_CONTENT + if(resultValue.error){ + response.status = NOT_ACCEPTABLE.value() + }else{ + response.status = OK.value() } + + + respond resultValue + } + + @Transactional + def updateMultiple() { + def settings = request.JSON + + settings.each{ settingData -> + Settings settingsInstance = Settings.get(settingData?.id) + settingsInstance.properties = settingData + settingsInstance.save failOnError: true + } + + respond settings + } } diff --git a/grails-app/controllers/streama/TheMovieDbController.groovy b/grails-app/controllers/streama/TheMovieDbController.groovy index 8867b50bf..822993c00 100644 --- a/grails-app/controllers/streama/TheMovieDbController.groovy +++ b/grails-app/controllers/streama/TheMovieDbController.groovy @@ -3,18 +3,18 @@ package streama import groovy.json.JsonSlurper class TheMovieDbController { - + def theMovieDbService def search() { String type = params.type String name = params.name - + def query = URLEncoder.encode(name, "UTF-8") - + def JsonContent = new URL(theMovieDbService.BASE_URL + '/search/' + type + '?query=' + query + '&api_key=' + theMovieDbService.API_KEY).text def json = new JsonSlurper().parseText(JsonContent) - + respond json?.results } @@ -28,7 +28,7 @@ class TheMovieDbController { def episodes = json?.episodes def result = [] - + episodes?.each{ episodeData -> Episode episode = new Episode(episodeData) episode.show = tvShow @@ -36,10 +36,10 @@ class TheMovieDbController { result.add(episode) } - + respond result - + } - - + + } diff --git a/grails-app/controllers/streama/UserController.groovy b/grails-app/controllers/streama/UserController.groovy index 0c14fe643..d81c273f7 100644 --- a/grails-app/controllers/streama/UserController.groovy +++ b/grails-app/controllers/streama/UserController.groovy @@ -6,7 +6,7 @@ import grails.transaction.Transactional @Transactional(readOnly = true) class UserController { - + def validationService def springSecurityService @@ -54,11 +54,11 @@ class UserController { def checkAvailability() { def username = params.username def result = [:] - + if(User.findByUsername(username)){ result.error = "User with that E-Mail-Address already exists." } - + respond result } @@ -78,9 +78,9 @@ class UserController { if(!userInstance.invitationSent && userInstance.enabled){ userInstance.uuid = randomUUID() as String - + log.debug("invitation email sent to $userInstance.username") - + try{ sendMail { to userInstance.username @@ -98,25 +98,29 @@ class UserController { respond userInstance, [status: CREATED] } + @Transactional def makeUserAdmin(User userInstance) { - + User currentUser = springSecurityService.currentUser - + if (userInstance == null) { render status: NOT_FOUND return } - + Role adminRole = Role.findByAuthority("ROLE_ADMIN") - + if(!currentUser.authorities?.contains(adminRole)){ render status: UNAUTHORIZED - return + return } UserRole.create(userInstance, adminRole, true) respond userInstance, [status: OK] } + } + + diff --git a/grails-app/services/streama/DefaultDataService.groovy b/grails-app/services/streama/DefaultDataService.groovy index 3450a868b..16d0dc02f 100644 --- a/grails-app/services/streama/DefaultDataService.groovy +++ b/grails-app/services/streama/DefaultDataService.groovy @@ -23,7 +23,7 @@ class DefaultDataService { role: Role.findByAuthority("ROLE_ADMIN") ] ] - + users.each{ userData -> if(!User.findByUsername(userData.username)){ def user = new User(username: userData.username, password: userData.password, enabled: userData.enabled) @@ -39,8 +39,7 @@ class DefaultDataService { def settings = [ [ settingsKey: 'Upload Directory', - value: '/data/streama', - description: 'This setting provides the application with your desired upload-path for all files.', + description: 'This setting provides the application with your desired upload-path for all files. The default so far has been /data/streama', ], [ settingsKey: 'TheMovieDB API key', diff --git a/grails-app/services/streama/SettingsService.groovy b/grails-app/services/streama/SettingsService.groovy new file mode 100644 index 000000000..35c578765 --- /dev/null +++ b/grails-app/services/streama/SettingsService.groovy @@ -0,0 +1,59 @@ +package streama + +import grails.transaction.Transactional + +@Transactional +class SettingsService { + + def theMovieDbService + + def validate(Settings settingsInstance) { + def resultValue = [:] + + if(settingsInstance.settingsKey == 'Upload Directory'){ + validateUploadDirectoryPermissions(settingsInstance, resultValue) + } + if(settingsInstance.settingsKey == 'TheMovieDB API key'){ + validateTheMovieDbAPI(settingsInstance, resultValue) + } + + return resultValue; + } + + + + def validateUploadDirectoryPermissions(Settings settingsInstance, resultValue){ + def uploadDir = new java.io.File(settingsInstance.value + '/upload') + try{ + uploadDir.mkdirs() + if(uploadDir.canWrite()){ + resultValue.success = true; + resultValue.message = "The directory was successfully accessed by the application"; + }else{ + resultValue.error = true; + resultValue.message = "The directory could not be accessed by the application. Please make sure that the directory exists and that you set the correct permissions."; + } + } + catch (Exception io){ + resultValue.error = true; + resultValue.message = "The directory could not be accessed by the application. Please make sure that the directory exists and that you set the correct permissions."; + } + } + + + def validateTheMovieDbAPI(Settings settingsInstance, resultValue){ + try{ + theMovieDbService.validateApiKey(settingsInstance.value) + resultValue.success = true; + resultValue.message = "The API-Key is valid and can be used!"; + } + + catch (Exception io){ + resultValue.error = true; + resultValue.message = "Invalid API key: You must be granted a valid key."; + } + } + +} + + diff --git a/grails-app/services/streama/TheMovieDbService.groovy b/grails-app/services/streama/TheMovieDbService.groovy index 0737f2aaf..859b5a29a 100644 --- a/grails-app/services/streama/TheMovieDbService.groovy +++ b/grails-app/services/streama/TheMovieDbService.groovy @@ -1,15 +1,19 @@ package streama +import groovy.json.JsonSlurper import grails.transaction.Transactional @Transactional class TheMovieDbService { - - def grailsApplication - + def BASE_URL = "https://api.themoviedb.org/3" - + def getAPI_KEY(){ - return grailsApplication.config.streama["themoviedbAPI"] + return Settings.findBySettingsKey('TheMovieDB API key')?.value + } + + def validateApiKey(apiKey){ + def JsonContent = new URL(BASE_URL + '/configuration?api_key=' + apiKey).text + return new JsonSlurper().parseText(JsonContent) } } diff --git a/grails-app/services/streama/UploadService.groovy b/grails-app/services/streama/UploadService.groovy index d99f4afb7..0d87ec87c 100644 --- a/grails-app/services/streama/UploadService.groovy +++ b/grails-app/services/streama/UploadService.groovy @@ -11,6 +11,9 @@ class UploadService { def grailsApplication def grailsLinkGenerator + def getStoragePath(){ + return Settings.findBySettingsKey('Upload Directory')?.value + } def upload(DefaultMultipartHttpServletRequest request) { @@ -23,7 +26,7 @@ class UploadService { } java.io.File targetFile = new java.io.File(this.dir.uploadDir,sha256Hex+extension) rawFile.transferTo(targetFile) - + File file = createFileFromUpload(sha256Hex, rawFile, extension) return file } @@ -42,7 +45,7 @@ class UploadService { } def getDir() { - def imagePath = grailsApplication.config.streama["storage"].path + def imagePath = storagePath def uploadDir = new java.io.File(imagePath + '/upload') if (!uploadDir.exists()){ uploadDir.mkdirs() @@ -53,7 +56,7 @@ class UploadService { } String getPathWithoutExtension(String sha256Hex){ - def uploadDir = new java.io.File(grailsApplication.config.streama["storage"].path + '/upload') + def uploadDir = new java.io.File(storagePath + '/upload') return "$uploadDir/$sha256Hex" } @@ -69,6 +72,6 @@ class UploadService { return grailsLinkGenerator.serverBaseURL + "/file/serve/" + file.sha256Hex + file.extension } - - -} \ No newline at end of file + + +}