├── .editorconfig ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── build.gradle ├── checkstyle.xml ├── client ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── build.gradle ├── build │ ├── build.js │ ├── check-versions.js │ ├── dev-client.js │ ├── dev-server.js │ ├── load-minified.js │ ├── service-worker-dev.js │ ├── service-worker-prod.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── assets │ │ ├── MaterialIcons-Regular.ttf │ │ ├── MaterialIcons-Regular.woff │ │ ├── MaterialIcons-Regular.woff2 │ │ ├── camera-offline.svg │ │ └── logo.png │ ├── components │ │ ├── AppNavigation.vue │ │ ├── ArchiveButton.vue │ │ └── Dropdown.vue │ ├── main.js │ ├── pages │ │ ├── Livestream.vue │ │ ├── LivestreamDetail.vue │ │ ├── Liveview.vue │ │ ├── LiveviewDetail.vue │ │ ├── Login.vue │ │ ├── NotFound.vue │ │ ├── Recordings.vue │ │ ├── RecordingsDetail.vue │ │ └── Settings.vue │ ├── router │ │ └── index.js │ ├── services │ │ └── api.js │ └── utils │ │ └── urlUtils.js └── static │ ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg │ └── manifest.json ├── docs ├── configuration.md ├── developers.md ├── integration.md └── screenshots │ ├── screenshot1-thumbnail.png │ ├── screenshot1.png │ ├── screenshot2-thumbnail.png │ ├── screenshot2.png │ ├── screenshot3-thumbnail.png │ └── screenshot3.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── github │ │ └── _1element │ │ └── sc │ │ ├── SurveillanceCenterApplication.java │ │ ├── configuration │ │ ├── FTPClientConfiguration.java │ │ ├── Java8TimeDialectConfiguration.java │ │ ├── ModelMapperConfiguration.java │ │ ├── PasswordEncoderConfiguration.java │ │ ├── SFTPClientConfiguration.java │ │ ├── ServiceLocatorFactoryBeanConfiguration.java │ │ ├── StaticResourceConfiguration.java │ │ └── WebSecurityConfiguration.java │ │ ├── controller │ │ ├── SurveillanceApiController.java │ │ ├── SurveillanceAuthenticationController.java │ │ ├── SurveillanceFeedController.java │ │ ├── SurveillanceProxyController.java │ │ └── SurveillanceStreamGenerationController.java │ │ ├── domain │ │ ├── Camera.java │ │ ├── CameraFtp.java │ │ ├── CameraPicture.java │ │ ├── PushNotificationSetting.java │ │ ├── SurveillanceImage.java │ │ ├── SurveillanceProperties.java │ │ ├── UploadFtplet.java │ │ ├── pushnotification │ │ │ ├── PushNotificationClient.java │ │ │ ├── PushNotificationClientFactory.java │ │ │ └── PushoverClient.java │ │ └── remotecopy │ │ │ ├── AbstractFTPRemoteCopy.java │ │ │ ├── AbstractSFTPRemoteCopy.java │ │ │ ├── FTPRemoteCopy.java │ │ │ ├── FTPRemoteCopyCleanup.java │ │ │ ├── RemoteCopy.java │ │ │ ├── RemoteCopyCleanup.java │ │ │ ├── SFTPRemoteCopy.java │ │ │ └── SFTPRemoteCopyCleanup.java │ │ ├── dto │ │ ├── CameraResource.java │ │ ├── ImagesCameraSummaryResult.java │ │ ├── PushNotificationSettingResource.java │ │ ├── PushNotificationSettingUpdateResource.java │ │ ├── SurveillanceImageBulkUpdateResource.java │ │ ├── SurveillanceImageResource.java │ │ └── SurveillanceImageUpdateResource.java │ │ ├── events │ │ ├── ImageReceivedEvent.java │ │ ├── PushNotificationEvent.java │ │ └── RemoteCopyEvent.java │ │ ├── exception │ │ ├── CameraNotFoundException.java │ │ ├── FTPRemoteCopyException.java │ │ ├── ForbiddenException.java │ │ ├── PropertyNotFoundException.java │ │ ├── ProxyException.java │ │ ├── PushNotificationClientException.java │ │ ├── ResourceNotFoundException.java │ │ └── SFTPRemoteCopyException.java │ │ ├── properties │ │ ├── FTPRemoteCopyProperties.java │ │ ├── FtpProperties.java │ │ ├── ImageProperties.java │ │ ├── ImageThumbnailProperties.java │ │ ├── MqttProperties.java │ │ ├── MultiCameraAwareProperties.java │ │ ├── PushNotificationProperties.java │ │ ├── SFTPRemoteCopyProperties.java │ │ ├── StreamGenerationProperties.java │ │ └── SurveillanceSecurityProperties.java │ │ ├── repository │ │ ├── CameraRepository.java │ │ ├── PushNotificationSettingRepository.java │ │ └── SurveillanceImageRepository.java │ │ ├── security │ │ ├── JwtAuthenticationEntryPoint.java │ │ ├── JwtAuthenticationFilter.java │ │ └── JwtAuthenticationRequest.java │ │ ├── service │ │ ├── CleanupTasks.java │ │ ├── FileService.java │ │ ├── FtpService.java │ │ ├── HealthCheckTasks.java │ │ ├── JwtAuthenticationService.java │ │ ├── JwtUserDetailsService.java │ │ ├── MjpegGenerationService.java │ │ ├── ModelMappingService.java │ │ ├── MqttService.java │ │ ├── PushNotificationService.java │ │ ├── SurveillanceImageHandlerService.java │ │ ├── SurveillanceProxyService.java │ │ ├── SurveillanceService.java │ │ └── ThumbnailService.java │ │ └── utils │ │ ├── RestTemplateUtils.java │ │ └── URIConstants.java └── resources │ ├── application-example.properties │ ├── messages.properties │ ├── messages_de.properties │ └── templates │ ├── error.html │ └── feed-cameras.html └── test ├── java └── com │ └── github │ └── _1element │ └── sc │ ├── SurveillanceCenterApplicationTests.java │ ├── controller │ ├── SurveillanceApiControllerTest.java │ ├── SurveillanceAuthenticationControllerTest.java │ └── SurveillanceFeedControllerTest.java │ ├── domain │ ├── CameraPictureTest.java │ ├── UploadFtpletTest.java │ ├── pushnotification │ │ └── PushoverClientTest.java │ └── remotecopy │ │ ├── FTPRemoteCopyCleanupTest.java │ │ ├── FTPRemoteCopyTest.java │ │ ├── SFTPRemoteCopyCleanupTest.java │ │ └── SFTPRemoteCopyTest.java │ ├── properties │ └── MultiCameraAwarePropertiesTest.java │ ├── repository │ └── CameraRepositoryTest.java │ ├── security │ └── JwtAuthenticationEntryPointTest.java │ ├── service │ ├── CleanupTasksTest.java │ ├── FileServiceTest.java │ ├── JwtAuthenticationServiceTest.java │ ├── JwtUserDetailsServiceTest.java │ ├── ModelMappingServiceTest.java │ ├── MqttServiceTest.java │ ├── PushNotificationServiceTest.java │ ├── SurveillanceImageHandlerServiceTest.java │ └── SurveillanceServiceTest.java │ └── utils │ └── RestTemplateUtilsTest.java └── resources └── application.properties /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .project 3 | .classpath 4 | .idea/ 5 | .settings/ 6 | .gradle/ 7 | /build/ 8 | /logs/ 9 | /bin/ 10 | /out/ 11 | src/main/resources/application.properties 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | before_install: 5 | - chmod +x gradlew 6 | script: 7 | - ./gradlew clean build --stacktrace 8 | deploy: 9 | provider: releases 10 | api_key: 11 | secure: N0l8RIT3z9lrzDJw2T4sABZlvCfSmqIbQJhSs2Eg3z0203fETk8T0ziSx5q3FSWbaPITXh9BkpixQltPq1QsFKChmjlaL22nLtBV1F/6s9LVwhsxngShpVLBn3YCUpKwv83TBlSyyO+JTZO2Xxh0ng1jXQCVY+KFreC0iQj8+uRKn2t6/Tf44cYWaBZ+tUAUre3ICMAD8LlInhZ/yGocFB6IbvSEFWQNg2RIBoyFPosPZd8CtXMyJdm/E9lAOg9i5vsMJFLtRiRB/JdUhrPb7Upl5/90DDHzCAIjZBgjV19Ficzaq8Zj6Wd4Fzfy6yeu0RT5XNKFLKlDmDolBmgHUoMUPDUBtEy0ozm8/oo47n06S41AVeQj+ETsbKHQQj0gyKOTtSnzF19Ko7kJ3H3p6xTtAeRHjYZscHXod754Jt22suDkjfx9XUKJFuIrUKm2SRvSEj4wHNvgln5ugMamkyT8jyf48bPi3DAHsTmGysxKhiden+OzGkYZGCUTs5LFf7IFK9v1On2KTvv8GJt1vByfzF/6SLqk8TkvkZt6tnsbDrVfoAuoKcxYmO4EUH6P5lh0SEyAe/qZtST1wHDAAPdSnnA4AnqvMf2KpGbPT6nGOrrVmymy4fwVwJa+asuK7wDUKCgHAUJ79bNx4hDT2GiOwmZT+ZWL0ZIX4GBecjo= 12 | file_glob: true 13 | file: "build/libs/surveillancecenter*.jar" 14 | skip_cleanup: true 15 | on: 16 | repo: 1element/sc 17 | tags: true 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | First off, thank you for taking the time to contribute to Surveillance Center. 4 | 5 | There are many ways to contribute. For example, you might improve the 6 | documentation, submit bug reports and feature requests or write code. 7 | 8 | Here is a quick guide how to submit pull requests: 9 | 10 | 1. Fork the project on GitHub and check out your copy locally. 11 | 12 | 2. For developing new features and bug fixes, the `develop` branch should be 13 | pulled and built upon. 14 | 15 | 3. Commit your changes and push to your fork. 16 | 17 | 4. Go to GitHub and [create a pull request](https://github.com/1element/sc/compare) 18 | to the Surveillance Center repository. Select `develop` as the base branch 19 | you'd like to merge your changes into. 20 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.5.10.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | plugins { 14 | id 'com.gorylenko.gradle-git-properties' version '1.4.17' 15 | } 16 | 17 | apply plugin: 'java' 18 | apply plugin: 'eclipse' 19 | apply plugin: 'idea' 20 | apply plugin: 'org.springframework.boot' 21 | apply plugin: 'checkstyle' 22 | 23 | jar { 24 | baseName = 'surveillancecenter' 25 | version = '2.1.0' 26 | exclude('application.properties') 27 | from('client/dist') { 28 | into 'public' 29 | } 30 | } 31 | sourceCompatibility = 1.8 32 | targetCompatibility = 1.8 33 | 34 | processResources.dependsOn('client:build') 35 | 36 | repositories { 37 | mavenCentral() 38 | } 39 | 40 | dependencies { 41 | compile('org.springframework.boot:spring-boot-starter-thymeleaf') 42 | compile('org.springframework.boot:spring-boot-starter-data-jpa') 43 | compile('org.springframework.boot:spring-boot-starter-security') 44 | compile('org.springframework.boot:spring-boot-devtools') 45 | compile('org.springframework.boot:spring-boot-starter-actuator') 46 | compile('org.springframework.boot:spring-boot-starter-hateoas') 47 | compile('org.apache.ftpserver:ftpserver-core:1.0.6') 48 | compile('org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.0') 49 | compile('io.jsonwebtoken:jjwt:0.9.0') 50 | compile('commons-net:commons-net:3.5') 51 | compile('net.coobird:thumbnailator:0.4.8') 52 | compile('com.jcraft:jsch:0.1.54') 53 | compile('org.modelmapper:modelmapper:1.1.1') 54 | compile('org.hsqldb:hsqldb:2.3.3') 55 | compile('commons-io:commons-io:2.4') 56 | compile('org.apache.commons:commons-lang3:3.4') 57 | compile('com.google.guava:guava:19.0') 58 | compile('org.thymeleaf.extras:thymeleaf-extras-java8time:2.1.0.RELEASE') 59 | compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.3') 60 | testCompile('org.springframework.boot:spring-boot-starter-test') 61 | } 62 | 63 | checkstyle { 64 | configFile = 'checkstyle.xml' as File 65 | } 66 | 67 | eclipse { 68 | classpath { 69 | containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') 70 | containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' 71 | } 72 | } 73 | 74 | // test { 75 | // forkEvery = 1 76 | // } 77 | 78 | tasks.withType(JavaCompile) { 79 | options.compilerArgs << "-Xlint:unchecked" << "-Werror" 80 | } 81 | 82 | task wrapper(type: Wrapper) { 83 | gradleVersion = '4.3.1' 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": [ "istanbul" ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /client/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | extends: 'airbnb-base', 13 | // required to lint *.vue files 14 | plugins: [ 15 | 'html' 16 | ], 17 | // check if imports actually resolve 18 | 'settings': { 19 | 'import/resolver': { 20 | 'webpack': { 21 | 'config': 'build/webpack.base.conf.js' 22 | } 23 | } 24 | }, 25 | // add your custom rules here 26 | 'rules': { 27 | // don't require .vue extension when importing 28 | 'import/extensions': ['error', 'always', { 29 | 'js': 'never', 30 | 'vue': 'never' 31 | }], 32 | // allow optionalDependencies 33 | 'import/no-extraneous-dependencies': ['error', { 34 | 'optionalDependencies': ['test/unit/index.js'] 35 | }], 36 | // allow debugger during development 37 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 38 | // allow dangling underscore for some properties (hateoas + vue internal) 39 | 'no-underscore-dangle': ['error', { 40 | 'allow': ['_embedded', '_links', '_uid'] 41 | }] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | /gradle/ 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Editor directories and files 10 | .idea 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /client/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | // https://github.com/srs/gradle-node-plugin/ 3 | id "com.moowork.node" version "1.2.0" 4 | } 5 | 6 | node { 7 | // version of node to use 8 | version = '8.9.3' 9 | // version of npm to use 10 | npmVersion = '5.5.1' 11 | // download node using above parameters 12 | download = true 13 | // work directory for unpacking node 14 | workDir = file("${project.projectDir}/gradle/node") 15 | // work directory for npm 16 | npmWorkDir = file("${project.projectDir}/gradle/npm") 17 | // work directory where node_modules should be located 18 | nodeModulesDir = file("${project.projectDir}") 19 | } 20 | 21 | task build(type: NpmTask) { 22 | args = ['run', 'build'] 23 | } 24 | 25 | build.dependsOn(npm_install) 26 | -------------------------------------------------------------------------------- /client/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./check-versions')() 4 | 5 | process.env.NODE_ENV = 'production' 6 | 7 | const ora = require('ora') 8 | const rm = require('rimraf') 9 | const path = require('path') 10 | const chalk = require('chalk') 11 | const webpack = require('webpack') 12 | const config = require('../config') 13 | const webpackConfig = require('./webpack.prod.conf') 14 | 15 | const spinner = ora('building for production...') 16 | spinner.start() 17 | 18 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 19 | if (err) throw err 20 | webpack(webpackConfig, function (err, stats) { 21 | spinner.stop() 22 | if (err) throw err 23 | process.stdout.write(stats.toString({ 24 | colors: true, 25 | modules: false, 26 | children: false, 27 | chunks: false, 28 | chunkModules: false 29 | }) + '\n\n') 30 | 31 | console.log(chalk.cyan(' Build complete.\n')) 32 | console.log(chalk.yellow( 33 | ' Tip: built files are meant to be served over an HTTP server.\n' + 34 | ' Opening index.html over file:// won\'t work.\n' 35 | )) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /client/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const semver = require('semver') 5 | const packageConfig = require('../package.json') 6 | const shell = require('shelljs') 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | }, 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | for (let i = 0; i < versionRequirements.length; i++) { 30 | const mod = versionRequirements[i] 31 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 32 | warnings.push(mod.name + ': ' + 33 | chalk.red(mod.currentVersion) + ' should be ' + 34 | chalk.green(mod.versionRequirement) 35 | ) 36 | } 37 | } 38 | 39 | if (warnings.length) { 40 | console.log('') 41 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 42 | console.log() 43 | for (let i = 0; i < warnings.length; i++) { 44 | const warning = warnings[i] 45 | console.log(' ' + warning) 46 | } 47 | console.log() 48 | process.exit(1) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/build/dev-client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable */ 4 | require('eventsource-polyfill') 5 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 6 | 7 | hotClient.subscribe(function (event) { 8 | if (event.action === 'reload') { 9 | window.location.reload() 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /client/build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./check-versions')() 4 | 5 | const config = require('../config') 6 | if (!process.env.NODE_ENV) { 7 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 8 | } 9 | 10 | const opn = require('opn') 11 | const path = require('path') 12 | const express = require('express') 13 | const webpack = require('webpack') 14 | const proxyMiddleware = require('http-proxy-middleware') 15 | const webpackConfig = require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | const port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | const proxyTable = config.dev.proxyTable 24 | 25 | const app = express() 26 | const compiler = webpack(webpackConfig) 27 | 28 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: false 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | compiler.plugin('compilation', function (compilation) { 38 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 39 | hotMiddleware.publish({ action: 'reload' }) 40 | cb() 41 | }) 42 | }) 43 | 44 | // enable hot-reload and state-preserving 45 | // compilation error display 46 | app.use(hotMiddleware) 47 | 48 | // proxy api requests 49 | Object.keys(proxyTable).forEach(function (context) { 50 | let options = proxyTable[context] 51 | if (typeof options === 'string') { 52 | options = { target: options } 53 | } 54 | app.use(proxyMiddleware(options.filter || context, options)) 55 | }) 56 | 57 | // handle fallback for HTML5 history API 58 | app.use(require('connect-history-api-fallback')()) 59 | 60 | // serve webpack bundle output 61 | app.use(devMiddleware) 62 | 63 | // serve pure static assets 64 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 65 | app.use(staticPath, express.static('./static')) 66 | 67 | const uri = 'http://localhost:' + port 68 | 69 | let _resolve 70 | const readyPromise = new Promise(resolve => { 71 | _resolve = resolve 72 | }) 73 | 74 | console.log('> Starting dev server...') 75 | devMiddleware.waitUntilValid(() => { 76 | console.log('> Listening at ' + uri + '\n') 77 | // when env is testing, don't need open it 78 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 79 | opn(uri) 80 | } 81 | _resolve() 82 | }) 83 | 84 | const server = app.listen(port) 85 | 86 | module.exports = { 87 | ready: readyPromise, 88 | close: () => { 89 | server.close() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /client/build/load-minified.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const UglifyJS = require('uglify-es') 5 | 6 | module.exports = function(filePath) { 7 | const code = fs.readFileSync(filePath, 'utf-8') 8 | const result = UglifyJS.minify(code) 9 | if (result.error) return '' 10 | return result.code 11 | } 12 | -------------------------------------------------------------------------------- /client/build/service-worker-dev.js: -------------------------------------------------------------------------------- 1 | // This service worker file is effectively a 'no-op' that will reset any 2 | // previous service worker registered for the same host:port combination. 3 | // In the production build, this file is replaced with an actual service worker 4 | // file that will precache your site's local assets. 5 | // See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 6 | 7 | self.addEventListener('install', () => self.skipWaiting()); 8 | 9 | self.addEventListener('activate', () => { 10 | self.clients.matchAll({ type: 'window' }).then(windowClients => { 11 | for (let windowClient of windowClients) { 12 | // Force open pages to refresh, so that they have a chance to load the 13 | // fresh navigation response from the local dev server. 14 | windowClient.navigate(windowClient.url); 15 | } 16 | }); 17 | }); -------------------------------------------------------------------------------- /client/build/service-worker-prod.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // Check to make sure service workers are supported in the current browser, 5 | // and that the current page is accessed from a secure origin. Using a 6 | // service worker from an insecure origin will trigger JS console errors. 7 | var isLocalhost = Boolean(window.location.hostname === 'localhost' || 8 | // [::1] is the IPv6 localhost address. 9 | window.location.hostname === '[::1]' || 10 | // 127.0.0.1/8 is considered localhost for IPv4. 11 | window.location.hostname.match( 12 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 13 | ) 14 | ); 15 | 16 | window.addEventListener('load', function() { 17 | if ('serviceWorker' in navigator && 18 | (window.location.protocol === 'https:' || isLocalhost)) { 19 | navigator.serviceWorker.register('service-worker.js') 20 | .then(function(registration) { 21 | // updatefound is fired if service-worker.js changes. 22 | registration.onupdatefound = function() { 23 | // updatefound is also fired the very first time the SW is installed, 24 | // and there's no need to prompt for a reload at that point. 25 | // So check here to see if the page is already controlled, 26 | // i.e. whether there's an existing service worker. 27 | if (navigator.serviceWorker.controller) { 28 | // The updatefound event implies that registration.installing is set 29 | var installingWorker = registration.installing; 30 | 31 | installingWorker.onstatechange = function() { 32 | switch (installingWorker.state) { 33 | case 'installed': 34 | // At this point, the old content will have been purged and the 35 | // fresh content will have been added to the cache. 36 | // It's the perfect time to display a "New content is 37 | // available; please refresh." message in the page's interface. 38 | break; 39 | 40 | case 'redundant': 41 | throw new Error('The installing ' + 42 | 'service worker became redundant.'); 43 | 44 | default: 45 | // Ignore 46 | } 47 | }; 48 | } 49 | }; 50 | }).catch(function(e) { 51 | console.error('Error during service worker registration:', e); 52 | }); 53 | } 54 | }); 55 | })(); 56 | -------------------------------------------------------------------------------- /client/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const config = require('../config') 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | return path.posix.join(assetsSubDirectory, _path) 12 | } 13 | 14 | exports.cssLoaders = function (options) { 15 | options = options || {} 16 | 17 | const cssLoader = { 18 | loader: 'css-loader', 19 | options: { 20 | minimize: process.env.NODE_ENV === 'production', 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | // generate loader string to be used with extract text plugin 26 | function generateLoaders (loader, loaderOptions) { 27 | const loaders = [cssLoader] 28 | if (loader) { 29 | loaders.push({ 30 | loader: loader + '-loader', 31 | options: Object.assign({}, loaderOptions, { 32 | sourceMap: options.sourceMap 33 | }) 34 | }) 35 | } 36 | 37 | // Extract CSS when that option is specified 38 | // (which is the case during production build) 39 | if (options.extract) { 40 | return ExtractTextPlugin.extract({ 41 | use: loaders, 42 | fallback: 'vue-style-loader' 43 | }) 44 | } else { 45 | return ['vue-style-loader'].concat(loaders) 46 | } 47 | } 48 | 49 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 50 | return { 51 | css: generateLoaders(), 52 | postcss: generateLoaders(), 53 | less: generateLoaders('less'), 54 | sass: generateLoaders('sass', { indentedSyntax: true }), 55 | scss: generateLoaders('sass'), 56 | stylus: generateLoaders('stylus'), 57 | styl: generateLoaders('stylus') 58 | } 59 | } 60 | 61 | // Generate loaders for standalone style files (outside of .vue) 62 | exports.styleLoaders = function (options) { 63 | const output = [] 64 | const loaders = exports.cssLoaders(options) 65 | for (const extension in loaders) { 66 | const loader = loaders[extension] 67 | output.push({ 68 | test: new RegExp('\\.' + extension + '$'), 69 | use: loader 70 | }) 71 | } 72 | return output 73 | } 74 | -------------------------------------------------------------------------------- /client/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const isProduction = process.env.NODE_ENV === 'production' 6 | 7 | module.exports = { 8 | loaders: utils.cssLoaders({ 9 | sourceMap: isProduction 10 | ? config.build.productionSourceMap 11 | : config.dev.cssSourceMap, 12 | extract: isProduction 13 | }), 14 | transformToRequire: { 15 | video: ['src', 'poster'], 16 | source: 'src', 17 | img: 'src', 18 | image: 'xlink:href' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const utils = require('./utils') 5 | const config = require('../config') 6 | const vueLoaderConfig = require('./vue-loader.conf') 7 | 8 | function resolve (dir) { 9 | return path.join(__dirname, '..', dir) 10 | } 11 | 12 | module.exports = { 13 | entry: { 14 | app: './src/main.js' 15 | }, 16 | output: { 17 | path: config.build.assetsRoot, 18 | filename: '[name].js', 19 | publicPath: process.env.NODE_ENV === 'production' 20 | ? config.build.assetsPublicPath 21 | : config.dev.assetsPublicPath 22 | }, 23 | resolve: { 24 | extensions: ['.js', '.vue', '.json'], 25 | alias: { 26 | 'vue$': 'vue/dist/vue.esm.js', 27 | '@': resolve('src') 28 | } 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /imagesloaded\.pkgd\.min\.js$/, 34 | use: ['script-loader'] 35 | }, 36 | { 37 | test: /\.(js|vue)$/, 38 | loader: 'eslint-loader', 39 | enforce: 'pre', 40 | include: [resolve('src'), resolve('test')], 41 | options: { 42 | formatter: require('eslint-friendly-formatter') 43 | } 44 | }, 45 | { 46 | test: /\.vue$/, 47 | loader: 'vue-loader', 48 | options: vueLoaderConfig 49 | }, 50 | { 51 | test: /\.js$/, 52 | loader: 'babel-loader', 53 | include: [resolve('src'), resolve('test')] 54 | }, 55 | { 56 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 57 | loader: 'url-loader', 58 | options: { 59 | limit: 10000, 60 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 61 | } 62 | }, 63 | { 64 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 65 | loader: 'url-loader', 66 | options: { 67 | limit: 10000, 68 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 69 | } 70 | }, 71 | { 72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 73 | loader: 'url-loader', 74 | options: { 75 | limit: 10000, 76 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 77 | } 78 | } 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /client/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const utils = require('./utils') 6 | const webpack = require('webpack') 7 | const config = require('../config') 8 | const merge = require('webpack-merge') 9 | const baseWebpackConfig = require('./webpack.base.conf') 10 | const HtmlWebpackPlugin = require('html-webpack-plugin') 11 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 12 | 13 | // add hot-reload related code to entry chunks 14 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 15 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 16 | }) 17 | 18 | module.exports = merge(baseWebpackConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 21 | }, 22 | // cheap-module-eval-source-map is faster for development 23 | devtool: '#cheap-module-eval-source-map', 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env': config.dev.env 27 | }), 28 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 29 | new webpack.HotModuleReplacementPlugin(), 30 | new webpack.NoEmitOnErrorsPlugin(), 31 | // https://github.com/ampedandwired/html-webpack-plugin 32 | new HtmlWebpackPlugin({ 33 | filename: 'index.html', 34 | template: 'index.html', 35 | inject: true, 36 | serviceWorkerLoader: `` 38 | }), 39 | new FriendlyErrorsPlugin() 40 | ] 41 | }) 42 | -------------------------------------------------------------------------------- /client/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const merge = require('webpack-merge') 4 | const prodEnv = require('./prod.env') 5 | 6 | module.exports = merge(prodEnv, { 7 | NODE_ENV: '"development"', 8 | API_BASE_URL: '"/api/v1/"', 9 | APP_BASE_URL: '"/"', 10 | }) 11 | -------------------------------------------------------------------------------- /client/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | const path = require('path') 5 | 6 | module.exports = { 7 | build: { 8 | env: require('./prod.env'), 9 | index: path.resolve(__dirname, '../dist/index.html'), 10 | assetsRoot: path.resolve(__dirname, '../dist'), 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/sc/', 13 | productionSourceMap: true, 14 | // Gzip off by default as many popular static hosts such as 15 | // Surge or Netlify already gzip all static assets for you. 16 | // Before setting to `true`, make sure to: 17 | // npm install --save-dev compression-webpack-plugin 18 | productionGzip: false, 19 | productionGzipExtensions: ['js', 'css'], 20 | // Run the build command with an extra argument to 21 | // View the bundle analyzer report after build finishes: 22 | // `npm run build --report` 23 | // Set to `true` or `false` to always turn it on or off 24 | bundleAnalyzerReport: process.env.npm_config_report 25 | }, 26 | dev: { 27 | env: require('./dev.env'), 28 | port: 8081, 29 | autoOpenBrowser: true, 30 | assetsSubDirectory: 'static', 31 | assetsPublicPath: '/', 32 | proxyTable: {}, 33 | // CSS Sourcemaps off by default because relative paths are "buggy" 34 | // with this option, according to the CSS-Loader README 35 | // (https://github.com/webpack/css-loader#sourcemaps) 36 | // In our experience, they generally work as expected, 37 | // just be aware of this issue when enabling this option. 38 | cssSourceMap: false, 39 | proxyTable: { 40 | // Proxy all requests starting with /api to the spring boot tomcat server on port 8080. 41 | // This ensures that there is no same origin policy violation for development. 42 | '/api': { 43 | target: 'http://localhost:8080/sc', 44 | changeOrigin: true, 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"', 3 | API_BASE_URL: '"/sc/api/v1/"', 4 | APP_BASE_URL: '"/sc/"', 5 | } 6 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Surveillance Center 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% for (var chunk of webpack.chunks) { 24 | for (var file of chunk.files) { 25 | if (file.match(/\.(js|css)$/)) { %> 26 | <% }}} %> 27 | 28 | 29 | 32 |
33 | <%= htmlWebpackPlugin.options.serviceWorkerLoader %> 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sc", 3 | "version": "2.0.0", 4 | "description": "Surveillance Center", 5 | "author": "1element <1element@users.noreply.github.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "lint": "eslint --ext .js,.vue src" 12 | }, 13 | "dependencies": { 14 | "bootstrap-vue": "^1.4.0", 15 | "moment": "^2.20.1", 16 | "vue": "^2.5.2", 17 | "vue-images-loaded": "^1.1.2", 18 | "vue-progressbar": "^0.7.3", 19 | "vue-router": "^3.0.1", 20 | "vue-simple-spinner": "^1.2.8" 21 | }, 22 | "devDependencies": { 23 | "autoprefixer": "^7.1.5", 24 | "axios": "^0.17.1", 25 | "babel-core": "^6.26.0", 26 | "babel-eslint": "^8.0.1", 27 | "babel-loader": "^7.1.2", 28 | "babel-plugin-transform-runtime": "^6.23.0", 29 | "babel-preset-env": "^1.6.0", 30 | "babel-preset-stage-2": "^6.24.1", 31 | "babel-register": "^6.26.0", 32 | "bootstrap": "^4.0.0-beta.2", 33 | "chalk": "^2.1.0", 34 | "connect-history-api-fallback": "^1.4.0", 35 | "copy-webpack-plugin": "^4.1.1", 36 | "css-loader": "^0.28.7", 37 | "cssnano": "^3.10.0", 38 | "eslint": "^4.9.0", 39 | "eslint-config-airbnb-base": "^11.1.3", 40 | "eslint-friendly-formatter": "^3.0.0", 41 | "eslint-import-resolver-webpack": "^0.8.1", 42 | "eslint-loader": "^1.9.0", 43 | "eslint-plugin-html": "^3.2.2", 44 | "eslint-plugin-import": "^2.7.0", 45 | "eslint-plugin-node": "^5.2.0", 46 | "eventsource-polyfill": "^0.9.6", 47 | "express": "^4.16.2", 48 | "extract-text-webpack-plugin": "^3.0.0", 49 | "file-loader": "^1.1.5", 50 | "friendly-errors-webpack-plugin": "^1.6.1", 51 | "html-webpack-plugin": "^2.30.1", 52 | "http-proxy-middleware": "^0.17.4", 53 | "node-sass": "^4.7.2", 54 | "opn": "^5.1.0", 55 | "optimize-css-assets-webpack-plugin": "^3.2.0", 56 | "ora": "^1.3.0", 57 | "rimraf": "^2.6.2", 58 | "sass-loader": "^6.0.6", 59 | "script-loader": "^0.7.2", 60 | "semver": "^5.4.1", 61 | "shelljs": "^0.7.8", 62 | "style-loader": "^0.19.1", 63 | "sw-precache-webpack-plugin": "^0.11.4", 64 | "uglify-es": "^3.1.3", 65 | "url-loader": "^0.6.2", 66 | "vue-loader": "^13.3.0", 67 | "vue-style-loader": "^3.0.3", 68 | "vue-template-compiler": "^2.5.2", 69 | "webpack": "^3.7.1", 70 | "webpack-bundle-analyzer": "^2.9.0", 71 | "webpack-dev-middleware": "^1.12.0", 72 | "webpack-hot-middleware": "^2.19.1", 73 | "webpack-merge": "^4.1.0" 74 | }, 75 | "engines": { 76 | "node": ">= 4.0.0", 77 | "npm": ">= 3.0.0" 78 | }, 79 | "browserslist": [ 80 | "> 1%", 81 | "last 2 versions", 82 | "not ie <= 8" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 42 | 43 | 97 | -------------------------------------------------------------------------------- /client/src/assets/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/src/assets/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /client/src/assets/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/src/assets/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /client/src/assets/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/src/assets/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /client/src/assets/camera-offline.svg: -------------------------------------------------------------------------------- 1 | camera offline -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/src/components/AppNavigation.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 57 | 58 | 71 | -------------------------------------------------------------------------------- /client/src/components/ArchiveButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 80 | 81 | 84 | -------------------------------------------------------------------------------- /client/src/components/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 48 | 49 | 52 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue'; 4 | import BootstrapVue from 'bootstrap-vue'; 5 | import VueProgressBar from 'vue-progressbar'; 6 | import moment from 'moment'; 7 | import 'bootstrap/dist/css/bootstrap.css'; 8 | import 'bootstrap-vue/dist/bootstrap-vue.css'; 9 | import App from './App'; 10 | import router from './router'; 11 | 12 | Vue.config.productionTip = false; 13 | 14 | Vue.use(BootstrapVue); 15 | Vue.use(VueProgressBar); 16 | 17 | Vue.filter('formatDate', (value) => { 18 | if (!value) { 19 | return ''; 20 | } 21 | return moment(String(value)).format('YYYY-MM-DD HH:mm:ss'); 22 | }); 23 | 24 | /* eslint-disable no-new */ 25 | new Vue({ 26 | el: '#app', 27 | router, 28 | template: '', 29 | components: { App }, 30 | }); 31 | -------------------------------------------------------------------------------- /client/src/pages/Livestream.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /client/src/pages/LivestreamDetail.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 58 | 59 | 62 | -------------------------------------------------------------------------------- /client/src/pages/LiveviewDetail.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 113 | -------------------------------------------------------------------------------- /client/src/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 56 | 57 | 97 | -------------------------------------------------------------------------------- /client/src/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 40 | -------------------------------------------------------------------------------- /client/src/pages/RecordingsDetail.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 87 | -------------------------------------------------------------------------------- /client/src/pages/Settings.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 104 | -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Login from '@/pages/Login'; 4 | import Recordings from '@/pages/Recordings'; 5 | import RecordingsDetail from '@/pages/RecordingsDetail'; 6 | import Liveview from '@/pages/Liveview'; 7 | import LiveviewDetail from '@/pages/LiveviewDetail'; 8 | import Livestream from '@/pages/Livestream'; 9 | import LivestreamDetail from '@/pages/LivestreamDetail'; 10 | import Settings from '@/pages/Settings'; 11 | import NotFound from '@/pages/NotFound'; 12 | 13 | Vue.use(Router); 14 | 15 | export default new Router({ 16 | base: process.env.APP_BASE_URL, 17 | mode: 'history', 18 | routes: [ 19 | { 20 | path: '/', 21 | name: 'root', 22 | redirect: '/liveview', 23 | }, 24 | { 25 | path: '/login', 26 | name: 'login', 27 | component: Login, 28 | }, 29 | { 30 | path: '/recordings', 31 | name: 'recordings', 32 | component: Recordings, 33 | }, 34 | { 35 | path: '/recordings/:id', 36 | name: 'recordings-detail', 37 | component: RecordingsDetail, 38 | }, 39 | { 40 | path: '/liveview', 41 | name: 'liveview', 42 | component: Liveview, 43 | }, 44 | { 45 | path: '/liveview/:id', 46 | name: 'liveview-detail', 47 | component: LiveviewDetail, 48 | }, 49 | { 50 | path: '/livestream', 51 | name: 'livestream', 52 | component: Livestream, 53 | }, 54 | { 55 | path: '/livestream/:id', 56 | name: 'livestream-detail', 57 | component: LivestreamDetail, 58 | }, 59 | { 60 | path: '/settings', 61 | name: 'settings', 62 | component: Settings, 63 | }, 64 | { 65 | path: '*', 66 | name: 'not-found', 67 | component: NotFound, 68 | }, 69 | ], 70 | }); 71 | -------------------------------------------------------------------------------- /client/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import router from '../router'; 3 | 4 | export default function () { 5 | const axiosClient = axios.create({ 6 | baseURL: process.env.API_BASE_URL, 7 | }); 8 | 9 | axiosClient.interceptors.request.use((config) => { 10 | router.app.$Progress.start(); 11 | return config; 12 | }, error => Promise.reject(error)); 13 | 14 | axiosClient.interceptors.response.use((response) => { 15 | router.app.$Progress.finish(); 16 | return response; 17 | }, (error) => { 18 | router.app.$Progress.fail(); 19 | // if we receive an unauthorized response 20 | if (error.response.status === 401) { 21 | // and we are not trying to login 22 | if (!(error.config.method === 'post' && /api\/v\d\/auth/.test(error.config.url))) { 23 | // redirect to login page 24 | router.replace({ name: 'login' }); 25 | } 26 | } 27 | return Promise.reject(error); 28 | }); 29 | 30 | return axiosClient; 31 | } 32 | -------------------------------------------------------------------------------- /client/src/utils/urlUtils.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | /** 4 | * Append hash fragment with current time to the provided url. 5 | * This will prevent browsers to cache the (image) object. 6 | */ 7 | appendHashFragment(url) { 8 | let plainUrl = url; 9 | const hashIndex = url.indexOf('#'); 10 | if (hashIndex !== -1) { 11 | plainUrl = url.substring(0, hashIndex); 12 | } 13 | return `${plainUrl}#${new Date().getTime()}`; 14 | }, 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /client/static/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /client/static/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /client/static/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /client/static/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /client/static/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /client/static/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /client/static/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /client/static/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /client/static/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /client/static/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /client/static/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /client/static/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/client/static/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /client/static/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Surveillance Center", 3 | "short_name": "Surveillance Center", 4 | "icons": [ 5 | { 6 | "src": "/sc/static/img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/sc/static/img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/sc/", 17 | "display": "standalone", 18 | "background_color": "#343a40", 19 | "theme_color": "#343a40" 20 | } 21 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | You can configure Surveillance Center by editing the `application.properties` 4 | file and restart the application afterwards. 5 | 6 | There is an example file [application-example.properties](https://github.com/1element/sc/blob/master/src/main/resources/application-example.properties) 7 | with annotations that shows all configuration options. 8 | 9 | It's not necessary to change all default values, but there a few required ones. 10 | 11 | Most features like push notifications, camera health check or off-site backup 12 | are disabled by default. 13 | 14 | At least the following settings must be changed to reflect your setup: 15 | 16 | ``` 17 | ################### 18 | # Global settings # 19 | ################### 20 | # Absolute path to the directory to save images. 21 | sc.image.storage-dir=/home/surveillance/images/ 22 | 23 | # The username used to login. 24 | sc.security.username=admin 25 | 26 | # The password used to login. Make sure to change this! 27 | # This must be hashed with BCrypt. The default password below is 'password'. 28 | sc.security.password=$2a$04$xdRJiiGwwHEbSgs6ucM0DOOCVEUQVaKtB3UPO16.h65sCWzPlkFHC 29 | 30 | # Internal secret key used to sign the JWT token. 31 | # Simply change this to something else, you don't have to remember the secret. 32 | sc.security.secret=verySecretKeyChangeMe 33 | 34 | # List of available camera ids (comma separated, don't use any special characters). 35 | # Each camera id listed here must have it's own configuration key (sc.camera[id]), see below. 36 | sc.cameras.available=front,backyard 37 | 38 | ################### 39 | # Camera settings # 40 | ################### 41 | # This is the main configuration part. Each camera you want to use must be listed in sc.cameras.available 42 | # and configured here (camera id in brackets). 43 | 44 | ### Front door camera ### 45 | sc.camera[front].name=Front door 46 | 47 | # Camera host used for ping health check (see below). Only used when sc.healthcheck.enabled is set to true. 48 | sc.camera[front].host=192.168.1.30 49 | 50 | # Enable snapshot (live view) for camera. If enabled sc.camera[id].snapshot-url (see below) must be configured. 51 | sc.camera[front].snapshot-enabled=true 52 | 53 | # Enable live stream for camera. If enabled the snapshot url (see below) is used to generate a simple MJPEG stream 54 | # by requesting the JPG image periodically. 55 | sc.camera[front].stream-enabled=true 56 | 57 | # URL used to display snapshots (live view). This URL will not be exposed, all requests use the built-in proxy. 58 | sc.camera[front].snapshot-url=https://192.168.1.30/snapshot.cgi 59 | 60 | # Ftp username for incoming images. This is used for camera identification and must be unique! 61 | sc.camera[front].ftp.username=camera1 62 | 63 | # Ftp password for incoming images. 64 | sc.camera[front].ftp.password=password 65 | 66 | # Incoming ftp directory. This is the place where new surveillance images for this camera will be put for a short 67 | # period, until thumbnails are generated and they are moved to sc.image.storage-dir. 68 | sc.camera[front].ftp.incoming-dir=/home/surveillance/ftp/camera1/ 69 | 70 | ############### 71 | # Data source # 72 | ############### 73 | # Datasource url. If you want stick to the hsqldb, make sure the file path matches to your environment. 74 | # If there is no existing database it will be created for you on application startup. 75 | spring.datasource.url=jdbc:hsqldb:file:/home/surveillance/db/surveillance.db 76 | ``` 77 | 78 | After modifying the `application.properties` file don't forget to stop and 79 | restart the application to see your changes. 80 | -------------------------------------------------------------------------------- /docs/developers.md: -------------------------------------------------------------------------------- 1 | ## Development environment 2 | 3 | If you want to develop new features or make bug fixes and need to compile the 4 | source code yourself, here are some notes on how to setup a local development 5 | environment. 6 | 7 | Make sure you have the [Java 8 Development Kit](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) 8 | installed on your system. 9 | 10 | To grab the source code of Surveillance Center, first clone the git repository: 11 | 12 | ``` 13 | git clone https://github.com/1element/sc.git 14 | ``` 15 | 16 | If you later want to contribute your code and make a pull request, 17 | you probably want to fork the repository first and clone your own fork. 18 | There are some additional notes for this in the [contributing document](https://github.com/1element/sc/blob/master/CONTRIBUTING.md). 19 | 20 | Checkout the `develop` branch as this is the branch where development happens. 21 | 22 | ``` 23 | git checkout develop 24 | ``` 25 | 26 | 27 | ### Server development 28 | 29 | The server (backend) part is written in Java using the Spring Boot framework. 30 | 31 | Gradle is used to build the project. For local development you can run 32 | 33 | ``` 34 | ./gradlew bootRun 35 | ``` 36 | 37 | in the root directory of the project. This will start the embedded Tomcat 38 | server on port 8080 and make your changes available. Make sure you have 39 | a proper configured `application.properties` file, either in 40 | `src/main/resources/application.properties` or somewhere else accessible. 41 | 42 | 43 | ### Client development 44 | 45 | The client (frontend) part is a Single Page Application (SPA) with 46 | PWA features (Progressive Web Application) written in Javascript (Vue.js). 47 | 48 | The sources are located in the `client` subdirectory. 49 | 50 | For development you need [Node.js](https://nodejs.org/) and npm 51 | (Node Package Manager) installed on your system. Npm ships with Node.js, 52 | so you don't have to install it separately. 53 | 54 | First of all run npm to install the dependencies: 55 | 56 | ``` 57 | cd client 58 | npm install 59 | ``` 60 | 61 | After this you can run for development: 62 | 63 | ``` 64 | npm run dev 65 | ``` 66 | 67 | This will start a local web server on port 8081 with hot-reload, 68 | Lint-on-save, etc. 69 | 70 | API requests are proxied to the Spring Boot tomcat server on port 8080, so make 71 | sure this is also running. 72 | 73 | 74 | ### Build 75 | 76 | The final build and executable jar file packaging is completely done using 77 | Gradle. You don't need to have Node.js and npm installed on your system. The 78 | gradle-node-plugin will take care of this. 79 | 80 | To build the project run 81 | 82 | ``` 83 | ./gradlew build 84 | ``` 85 | 86 | in the root directory. 87 | -------------------------------------------------------------------------------- /docs/screenshots/screenshot1-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/docs/screenshots/screenshot1-thumbnail.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/docs/screenshots/screenshot1.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot2-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/docs/screenshots/screenshot2-thumbnail.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/docs/screenshots/screenshot2.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot3-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/docs/screenshots/screenshot3-thumbnail.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/docs/screenshots/screenshot3.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1element/sc/99b1a78c9abe1c2cf0b315ac9af475997b2791fc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Nov 19 16:47:31 CET 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'client' 2 | 3 | rootProject.name = 'surveillancecenter' 4 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/SurveillanceCenterApplication.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc; //NOSONAR 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; 7 | import org.springframework.scheduling.annotation.EnableAsync; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | @EntityScan(basePackageClasses = {SurveillanceCenterApplication.class, Jsr310JpaConverters.class}) 11 | @SpringBootApplication 12 | @EnableScheduling 13 | @EnableAsync 14 | public class SurveillanceCenterApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(SurveillanceCenterApplication.class, args); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/configuration/FTPClientConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.configuration; //NOSONAR 2 | 3 | import org.apache.commons.net.ftp.FTPClient; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Scope; 7 | 8 | /** 9 | * FTP client configuration bean. 10 | */ 11 | @Configuration 12 | public class FTPClientConfiguration { 13 | 14 | @Bean 15 | @Scope("prototype") 16 | public FTPClient ftpClient() { 17 | return new FTPClient(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/configuration/Java8TimeDialectConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.configuration; //NOSONAR 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect; 6 | 7 | /** 8 | * Configuration class to support Java 8 time dialect. 9 | */ 10 | @Configuration 11 | public class Java8TimeDialectConfiguration { 12 | 13 | @Bean 14 | public Java8TimeDialect java8TimeDialect() { 15 | return new Java8TimeDialect(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/configuration/ModelMapperConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.configuration; //NOSONAR 2 | 3 | import org.modelmapper.ModelMapper; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * ModelMapper configuration bean. 9 | * This allows mapping of entities/domain objects to DTOs (REST resources). 10 | */ 11 | @Configuration 12 | public class ModelMapperConfiguration { 13 | 14 | @Bean 15 | public ModelMapper modelMapper() { 16 | return new ModelMapper(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/configuration/PasswordEncoderConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.configuration; //NOSONAR 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | 8 | /** 9 | * Password encoder configuration to use BCrypt. 10 | */ 11 | @Configuration 12 | public class PasswordEncoderConfiguration { 13 | 14 | @Bean 15 | public PasswordEncoder passwordEncoder() { 16 | return new BCryptPasswordEncoder(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/configuration/SFTPClientConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.configuration; //NOSONAR 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Scope; 6 | 7 | import com.jcraft.jsch.JSch; 8 | 9 | /** 10 | * SFTP client configuration bean. 11 | */ 12 | @Configuration 13 | public class SFTPClientConfiguration { 14 | 15 | @Bean 16 | @Scope("prototype") 17 | public JSch jsch() { 18 | return new JSch(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/configuration/ServiceLocatorFactoryBeanConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.configuration; //NOSONAR 2 | 3 | import org.springframework.beans.factory.FactoryBean; 4 | import org.springframework.beans.factory.config.ServiceLocatorFactoryBean; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import com.github._1element.sc.domain.pushnotification.PushNotificationClientFactory; 9 | 10 | /** 11 | * Service locator for factory beans. 12 | */ 13 | @Configuration 14 | public class ServiceLocatorFactoryBeanConfiguration { 15 | 16 | /** 17 | * Service locator for factory beans. 18 | * 19 | * @return the factory bean 20 | */ 21 | @Bean 22 | public FactoryBean serviceLocatorFactoryBean() { 23 | ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean(); 24 | factoryBean.setServiceLocatorInterface(PushNotificationClientFactory.class); 25 | 26 | return factoryBean; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/configuration/StaticResourceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.configuration; //NOSONAR 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.io.Resource; 6 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 8 | 9 | import com.github._1element.sc.properties.ImageProperties; 10 | import org.springframework.web.servlet.resource.PathResourceResolver; 11 | 12 | import java.io.IOException; 13 | 14 | /** 15 | * Configuration for static resources (images and client). 16 | */ 17 | @Configuration 18 | public class StaticResourceConfiguration extends WebMvcConfigurerAdapter { 19 | 20 | public static final String IMAGES_PATH = "/images/"; 21 | 22 | private ImageProperties imageProperties; 23 | 24 | @Autowired 25 | public StaticResourceConfiguration(ImageProperties imageProperties) { 26 | this.imageProperties = imageProperties; 27 | } 28 | 29 | @Override 30 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 31 | // expose images directory as static resource 32 | registry.addResourceHandler(IMAGES_PATH + "**").addResourceLocations("file:" + imageProperties.getStorageDir()); 33 | 34 | // map static path to single page app static assets 35 | registry.addResourceHandler("/static/**").addResourceLocations("classpath:/public/static/"); 36 | 37 | // map all other paths to the single page app entry point (index.html) 38 | registry.addResourceHandler("/**").addResourceLocations("classpath:/public/index.html").resourceChain(true) 39 | .addResolver(new PathResourceResolver() { 40 | @Override 41 | protected Resource getResource(String resourcePath, Resource location) throws IOException { 42 | return location.exists() && location.isReadable() ? location : null; 43 | } 44 | }); 45 | 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/controller/SurveillanceAuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.controller; //NOSONAR 2 | 3 | import com.github._1element.sc.security.JwtAuthenticationRequest; 4 | import com.github._1element.sc.service.JwtAuthenticationService; 5 | import com.github._1element.sc.utils.URIConstants; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.security.core.context.SecurityContextHolder; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.ResponseStatus; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import javax.servlet.http.Cookie; 18 | import javax.servlet.http.HttpServletResponse; 19 | 20 | /** 21 | * REST controller to handle authentication login. 22 | */ 23 | @RestController 24 | @RequestMapping(URIConstants.API_ROOT) 25 | public class SurveillanceAuthenticationController { 26 | 27 | private final JwtAuthenticationService jwtAuthenticationService; 28 | 29 | @Autowired 30 | public SurveillanceAuthenticationController(final JwtAuthenticationService jwtAuthenticationService) { 31 | this.jwtAuthenticationService = jwtAuthenticationService; 32 | } 33 | 34 | /** 35 | * Endpoint to create an authentication token, that will be returned as an http-only cookie. 36 | * 37 | * @param authenticationRequest the authentication request with username and password 38 | * @param response the http response 39 | * 40 | * @throws AuthenticationException if authentication failed 41 | */ 42 | @PostMapping(URIConstants.API_AUTH) 43 | @ResponseStatus(HttpStatus.NO_CONTENT) 44 | public void createAuthenticationToken(@RequestBody final JwtAuthenticationRequest authenticationRequest, 45 | final HttpServletResponse response) throws AuthenticationException { 46 | 47 | final Authentication authentication = jwtAuthenticationService.attemptAuthentication( 48 | authenticationRequest.getUsername(), authenticationRequest.getPassword()); 49 | 50 | SecurityContextHolder.getContext().setAuthentication(authentication); 51 | 52 | final Cookie cookie = jwtAuthenticationService.generateTokenCookie(authenticationRequest.getUsername()); 53 | response.addCookie(cookie); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/controller/SurveillanceFeedController.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.controller; //NOSONAR 2 | 3 | import com.github._1element.sc.dto.ImagesCameraSummaryResult; 4 | import com.github._1element.sc.service.SurveillanceService; 5 | import com.github._1element.sc.utils.URIConstants; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | 12 | import java.util.List; 13 | 14 | @Controller 15 | @RequestMapping(URIConstants.FEED_ROOT) 16 | public class SurveillanceFeedController { 17 | 18 | private final SurveillanceService surveillanceService; 19 | 20 | @Autowired 21 | public SurveillanceFeedController(final SurveillanceService surveillanceService) { 22 | this.surveillanceService = surveillanceService; 23 | } 24 | 25 | /** 26 | * Renders RSS status feed displaying a summary for each camera. 27 | * 28 | * @param model the spring model 29 | * @return rendered RSS feed 30 | */ 31 | @GetMapping(URIConstants.FEED_CAMERAS) 32 | public String camerasfeed(final Model model) { 33 | final List imagesCameraSummaryResult = surveillanceService.getImagesCameraSummary(); 34 | 35 | model.addAttribute("imagesCameraSummaryResult", imagesCameraSummaryResult); 36 | 37 | return "feed-cameras"; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/controller/SurveillanceProxyController.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.controller; //NOSONAR 2 | 3 | import com.github._1element.sc.domain.Camera; 4 | import com.github._1element.sc.exception.CameraNotFoundException; 5 | import com.github._1element.sc.exception.ProxyException; 6 | import com.github._1element.sc.repository.CameraRepository; 7 | import com.github._1element.sc.service.SurveillanceProxyService; 8 | import com.github._1element.sc.utils.URIConstants; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | 16 | @Controller 17 | @RequestMapping(URIConstants.PROXY_ROOT) 18 | public class SurveillanceProxyController { 19 | 20 | private final SurveillanceProxyService proxyService; 21 | 22 | private final CameraRepository cameraRepository; 23 | 24 | @Autowired 25 | public SurveillanceProxyController(final SurveillanceProxyService proxyService, 26 | final CameraRepository cameraRepository) { 27 | this.proxyService = proxyService; 28 | this.cameraRepository = cameraRepository; 29 | } 30 | 31 | /** 32 | * Retrieves a new snapshot for the provided camera (proxy). 33 | * 34 | * @param id the camera id to retrieve snapshot for 35 | * @return retrieved snapshot image 36 | * @throws CameraNotFoundException if camera id was not found 37 | */ 38 | @GetMapping(URIConstants.PROXY_SNAPSHOT) 39 | public ResponseEntity retrieveSnapshot(@PathVariable final String id) 40 | throws CameraNotFoundException, ProxyException { 41 | 42 | final Camera camera = cameraRepository.findById(id); 43 | 44 | if (camera == null) { 45 | throw new CameraNotFoundException(); 46 | } 47 | 48 | return proxyService.retrieveImage(camera.getPicture().getSnapshotUrl()); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/controller/SurveillanceStreamGenerationController.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.controller; //NOSONAR 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.http.HttpServletResponse; 6 | 7 | import com.github._1element.sc.domain.Camera; 8 | import com.github._1element.sc.exception.CameraNotFoundException; 9 | import com.github._1element.sc.repository.CameraRepository; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.client.RestClientException; 18 | 19 | import com.github._1element.sc.exception.ForbiddenException; 20 | import com.github._1element.sc.service.MjpegGenerationService; 21 | import com.github._1element.sc.utils.URIConstants; 22 | 23 | @Controller 24 | @RequestMapping(URIConstants.GENERATE_ROOT) 25 | public class SurveillanceStreamGenerationController { 26 | 27 | private final CameraRepository cameraRepository; 28 | 29 | private final MjpegGenerationService mjpegGenerationService; 30 | 31 | private static final Logger LOG = LoggerFactory.getLogger(SurveillanceStreamGenerationController.class); 32 | 33 | /** 34 | * Constructs a SurveillanceStreamGenerationController. 35 | * 36 | * @param cameraRepository the camera repository 37 | * @param mjpegGenerationService the MJPEG generation service 38 | */ 39 | @Autowired 40 | public SurveillanceStreamGenerationController(final CameraRepository cameraRepository, 41 | final MjpegGenerationService mjpegGenerationService) { 42 | this.cameraRepository = cameraRepository; 43 | this.mjpegGenerationService = mjpegGenerationService; 44 | } 45 | 46 | /** 47 | * Creates a simple MJPEG stream by requesting a camera snapshot JPG URL periodically. 48 | * 49 | * @param id the camera id to create stream for 50 | * @param response the streaming HTTP response 51 | * @throws ForbiddenException exception if MJPEG stream is disabled by configuration 52 | * @throws CameraNotFoundException exception if provided camera id could not be found 53 | */ 54 | @GetMapping(URIConstants.GENERATE_MJPEG) 55 | public void generateMJPEG(@PathVariable final String id, final HttpServletResponse response) 56 | throws ForbiddenException, CameraNotFoundException { 57 | 58 | final Camera camera = cameraRepository.findById(id); 59 | 60 | if (camera == null) { 61 | throw new CameraNotFoundException(); 62 | } 63 | 64 | if (!camera.getPicture().isStreamEnabled()) { 65 | throw new ForbiddenException("MJPEG stream generation is disabled."); 66 | } 67 | 68 | mjpegGenerationService.setContentType(response); 69 | mjpegGenerationService.setCacheControlHeader(response); 70 | 71 | try { 72 | mjpegGenerationService.writeSnapshotToOutputStream(camera.getPicture().getSnapshotUrl(), response); 73 | } catch (IOException | RestClientException exception) { 74 | LOG.debug("MJPEG streaming terminated: {}", exception.getMessage()); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/Camera.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain; //NOSONAR 2 | 3 | /** 4 | * Surveillance camera POJO. 5 | */ 6 | public class Camera { 7 | 8 | private final String id; 9 | 10 | private final String name; 11 | 12 | private final String host; 13 | 14 | private final String mqttTopic; 15 | 16 | private final CameraFtp ftp; 17 | 18 | private final CameraPicture picture; 19 | 20 | /** 21 | * Constructs a new camera. 22 | * 23 | * @param id the unique id of the camera 24 | * @param name the camera name 25 | * @param host the (internal) host the camera is running on 26 | * @param mqttTopic the mqtt topic 27 | * @param ftp the ftp settings 28 | * @param picture the picture settings 29 | */ 30 | public Camera(final String id, final String name, final String host, 31 | final String mqttTopic, final CameraFtp ftp, final CameraPicture picture) { 32 | this.id = id; 33 | this.name = name; 34 | this.host = host; 35 | this.ftp = ftp; 36 | this.picture = picture; 37 | this.mqttTopic = mqttTopic; 38 | } 39 | 40 | public String getId() { 41 | return id; 42 | } 43 | 44 | public String getName() { 45 | return name; 46 | } 47 | 48 | public String getHost() { 49 | return host; 50 | } 51 | 52 | public CameraFtp getFtp() { 53 | return ftp; 54 | } 55 | 56 | public CameraPicture getPicture() { 57 | return picture; 58 | } 59 | 60 | public String getMqttTopic() { 61 | return mqttTopic; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/CameraFtp.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain; //NOSONAR 2 | 3 | /** 4 | * Camera ftp value object. 5 | */ 6 | public class CameraFtp { 7 | 8 | private final String username; 9 | private final String password; 10 | private final String incomingDirectory; 11 | 12 | /** 13 | * Constructs a new camera ftp value object. 14 | * 15 | * @param username the ftp username for incoming files 16 | * @param password the ftp password for incoming files 17 | * @param incomingDirectory the ftp incoming directory 18 | */ 19 | public CameraFtp(final String username, final String password, final String incomingDirectory) { 20 | this.username = username; 21 | this.password = password; 22 | this.incomingDirectory = incomingDirectory; 23 | } 24 | 25 | public String getUsername() { 26 | return username; 27 | } 28 | 29 | public String getPassword() { 30 | return password; 31 | } 32 | 33 | public String getIncomingDirectory() { 34 | return incomingDirectory; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/CameraPicture.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain; //NOSONAR 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | /** 6 | * Camera picture value object. 7 | */ 8 | public class CameraPicture { 9 | 10 | private final String snapshotUrl; 11 | private final boolean snapshotEnabled; 12 | private final boolean streamEnabled; 13 | 14 | /** 15 | * Constructs a new camera picture value object. 16 | * 17 | * @param snapshotUrl optional url to retrieve snapshots 18 | * @param snapshotEnabled true if snapshots are enabled 19 | * @param streamEnabled true if streaming is enabled 20 | */ 21 | public CameraPicture(final String snapshotUrl, final boolean snapshotEnabled, final boolean streamEnabled) { 22 | if ((snapshotEnabled || streamEnabled) && StringUtils.isBlank(snapshotUrl)) { 23 | throw new IllegalArgumentException("Snapshot-url must be provided if snapshot-enabled or stream-enabled."); 24 | } 25 | 26 | this.snapshotUrl = snapshotUrl; 27 | this.snapshotEnabled = snapshotEnabled; 28 | this.streamEnabled = streamEnabled; 29 | } 30 | 31 | public String getSnapshotUrl() { 32 | return snapshotUrl; 33 | } 34 | 35 | public boolean isSnapshotEnabled() { 36 | return snapshotEnabled; 37 | } 38 | 39 | public boolean isStreamEnabled() { 40 | return streamEnabled; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/PushNotificationSetting.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain; //NOSONAR 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | 8 | /** 9 | * Entity for push notification settings. 10 | */ 11 | @Entity 12 | public class PushNotificationSetting { 13 | 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.AUTO) 16 | private long id; 17 | 18 | private String cameraId; 19 | 20 | private boolean enabled = false; 21 | 22 | protected PushNotificationSetting() { 23 | } 24 | 25 | public PushNotificationSetting(final String cameraId, final boolean enabled) { 26 | this.cameraId = cameraId; 27 | this.enabled = enabled; 28 | } 29 | 30 | public String getCameraId() { 31 | return cameraId; 32 | } 33 | 34 | public boolean isEnabled() { 35 | return enabled; 36 | } 37 | 38 | public void setEnabled(final boolean enabled) { 39 | this.enabled = enabled; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/SurveillanceImage.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain; //NOSONAR 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | import java.time.LocalDateTime; 8 | import java.util.Objects; 9 | 10 | /** 11 | * Surveillance image entity. 12 | */ 13 | @Entity 14 | public class SurveillanceImage { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.AUTO) 18 | private long id; 19 | 20 | private String fileName; 21 | 22 | private String cameraId; 23 | 24 | private LocalDateTime receivedAt; 25 | 26 | private final boolean archived = false; 27 | 28 | protected SurveillanceImage() { 29 | } 30 | 31 | /** 32 | * Constructs a new surveillance image. 33 | * 34 | * @param fileName the image file name 35 | * @param cameraId the camera identifier 36 | * @param receivedAt the received at time 37 | */ 38 | public SurveillanceImage(final String fileName, final String cameraId, final LocalDateTime receivedAt) { 39 | this.fileName = fileName; 40 | this.cameraId = cameraId; 41 | this.receivedAt = receivedAt; 42 | } 43 | 44 | public long getId() { 45 | return id; 46 | } 47 | 48 | public String getFileName() { 49 | return fileName; 50 | } 51 | 52 | public LocalDateTime getReceivedAt() { 53 | return receivedAt; 54 | } 55 | 56 | public boolean isArchived() { 57 | return archived; 58 | } 59 | 60 | public String getCameraId() { 61 | return cameraId; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return String.valueOf(id); 67 | } 68 | 69 | @Override 70 | public boolean equals(final Object other) { 71 | if (!(other instanceof SurveillanceImage)) { 72 | return false; 73 | } 74 | final SurveillanceImage castOther = (SurveillanceImage) other; 75 | return Objects.equals(id, castOther.id) && Objects.equals(fileName, castOther.fileName) 76 | && Objects.equals(cameraId, castOther.cameraId) && Objects.equals(receivedAt, castOther.receivedAt) 77 | && Objects.equals(archived, castOther.archived); 78 | } 79 | 80 | @Override 81 | public int hashCode() { 82 | return Objects.hash(id, fileName, cameraId, receivedAt, archived); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/SurveillanceProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain; //NOSONAR 2 | 3 | import com.github._1element.sc.configuration.StaticResourceConfiguration; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 6 | 7 | /** 8 | * Surveillance properties component class. 9 | * This will expose some programmatic properties. 10 | */ 11 | @Component 12 | public class SurveillanceProperties { 13 | 14 | private static final String IMAGE_THUMBNAIL_PREFIX = "thumbnail."; 15 | 16 | public String getImageThumbnailPrefix() { 17 | return IMAGE_THUMBNAIL_PREFIX; 18 | } 19 | 20 | public String getImageBaseUrl() { 21 | return ServletUriComponentsBuilder.fromCurrentContextPath().path(StaticResourceConfiguration.IMAGES_PATH) 22 | .build().toString(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/pushnotification/PushNotificationClient.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.pushnotification; //NOSONAR 2 | 3 | import com.github._1element.sc.exception.PushNotificationClientException; 4 | 5 | /** 6 | * Interface for all push notification clients. 7 | */ 8 | public interface PushNotificationClient { 9 | 10 | void sendMessage(String title, String text) throws PushNotificationClientException; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/pushnotification/PushNotificationClientFactory.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.pushnotification; //NOSONAR 2 | 3 | /** 4 | * Factory to create a push notification client adapter. 5 | */ 6 | public interface PushNotificationClientFactory { 7 | 8 | /** 9 | * Returns the push notification client. 10 | * 11 | * @param name the client adapter name 12 | * @return the push notification client 13 | */ 14 | public PushNotificationClient getClient(String name); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/pushnotification/PushoverClient.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.pushnotification; //NOSONAR 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.web.client.RestTemplateBuilder; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.util.LinkedMultiValueMap; 9 | import org.springframework.util.MultiValueMap; 10 | import org.springframework.web.client.RestClientException; 11 | import org.springframework.web.client.RestTemplate; 12 | 13 | import com.github._1element.sc.exception.PushNotificationClientException; 14 | import com.github._1element.sc.properties.PushNotificationProperties; 15 | 16 | /** 17 | * Pushover client adapter. 18 | * This will send push notifications using the pushover.net API. 19 | */ 20 | @Component("pushover") 21 | public class PushoverClient implements PushNotificationClient { 22 | 23 | private final RestTemplate restTemplate; 24 | 25 | private PushNotificationProperties properties; 26 | 27 | private static final String ENDPOINT = "https://api.pushover.net/1/messages.json"; 28 | 29 | private static final String PARAM_API_TOKEN = "token"; 30 | 31 | private static final String PARAM_USER_TOKEN = "user"; 32 | 33 | private static final String PARAM_TITLE = "title"; 34 | 35 | private static final String PARAM_MESSAGE = "message"; 36 | 37 | private static final Logger LOG = LoggerFactory.getLogger(PushoverClient.class); 38 | 39 | @Autowired 40 | public PushoverClient(RestTemplateBuilder restTemplateBuilder, PushNotificationProperties properties) { 41 | this.restTemplate = restTemplateBuilder.build(); 42 | this.properties = properties; 43 | } 44 | 45 | @Override 46 | public void sendMessage(String title, String text) throws PushNotificationClientException { 47 | // pushover does not support receiving json 48 | // so use a MultiValueMap that will be converted to application/x-www-form-urlencoded 49 | MultiValueMap requestParams = new LinkedMultiValueMap<>(); 50 | requestParams.add(PARAM_API_TOKEN, properties.getApiToken()); 51 | requestParams.add(PARAM_USER_TOKEN, properties.getUserToken()); 52 | requestParams.add(PARAM_TITLE, title); 53 | requestParams.add(PARAM_MESSAGE, text); 54 | 55 | try { 56 | restTemplate.postForObject(ENDPOINT, requestParams, Void.class); 57 | } catch (RestClientException exception) { 58 | LOG.error("Error while sending push notification: {}", exception.getMessage()); 59 | throw new PushNotificationClientException(exception); 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/remotecopy/AbstractFTPRemoteCopy.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.remotecopy; //NOSONAR 2 | 3 | import java.io.IOException; 4 | 5 | import org.apache.commons.net.ftp.FTP; 6 | import org.apache.commons.net.ftp.FTPClient; 7 | import org.apache.commons.net.ftp.FTPReply; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | 10 | import com.github._1element.sc.exception.FTPRemoteCopyException; 11 | import com.github._1element.sc.properties.FTPRemoteCopyProperties; 12 | import com.github._1element.sc.service.FileService; 13 | 14 | /** 15 | * Abstract FTP remote copy class. 16 | */ 17 | public abstract class AbstractFTPRemoteCopy { 18 | 19 | protected FTPRemoteCopyProperties ftpRemoteCopyProperties; 20 | 21 | protected FTPClient ftp; 22 | 23 | protected FileService fileService; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @param ftpRemoteCopyProperties the properties to use for remote copying 29 | * @param ftp the ftp client dependency 30 | * @param fileService the file service dependency 31 | */ 32 | @Autowired 33 | public AbstractFTPRemoteCopy(FTPRemoteCopyProperties ftpRemoteCopyProperties, FTPClient ftp, 34 | FileService fileService) { 35 | this.ftpRemoteCopyProperties = ftpRemoteCopyProperties; 36 | this.ftp = ftp; 37 | this.fileService = fileService; 38 | } 39 | 40 | /** 41 | * Connect to FTP server. 42 | * 43 | * @throws FTPRemoteCopyException exception if connection or login to remote was not successful 44 | * @throws IOException exception if IO error occurred during connection 45 | */ 46 | protected void connect() throws FTPRemoteCopyException, IOException { 47 | ftp.connect(ftpRemoteCopyProperties.getHost()); 48 | 49 | if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) { 50 | throw new FTPRemoteCopyException(String.format("Could not connect to remote ftp server '%s'. Response was: %s", 51 | ftpRemoteCopyProperties.getHost(), ftp.getReplyString())); 52 | } 53 | 54 | if (!ftp.login(ftpRemoteCopyProperties.getUsername(), ftpRemoteCopyProperties.getPassword())) { 55 | throw new FTPRemoteCopyException("Could not login to remote ftp server. Invalid username or password."); 56 | } 57 | 58 | ftp.setFileType(FTP.BINARY_FILE_TYPE); 59 | ftp.enterLocalPassiveMode(); 60 | } 61 | 62 | /** 63 | * Disconnect from FTP server. 64 | */ 65 | protected void disconnect() { 66 | if (ftp != null && ftp.isConnected()) { 67 | try { 68 | ftp.logout(); 69 | ftp.disconnect(); 70 | } catch (IOException exception) { 71 | // silently ignore disconnect exceptions 72 | } 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/remotecopy/AbstractSFTPRemoteCopy.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.remotecopy; //NOSONAR 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import com.github._1element.sc.exception.SFTPRemoteCopyException; 6 | import com.github._1element.sc.properties.SFTPRemoteCopyProperties; 7 | import com.github._1element.sc.service.FileService; 8 | import com.jcraft.jsch.Channel; 9 | import com.jcraft.jsch.ChannelSftp; 10 | import com.jcraft.jsch.JSch; 11 | import com.jcraft.jsch.JSchException; 12 | import com.jcraft.jsch.Session; 13 | 14 | /** 15 | * Abstract SFTP remote copy class. 16 | */ 17 | public class AbstractSFTPRemoteCopy { 18 | 19 | protected SFTPRemoteCopyProperties sftpRemoteCopyProperties; 20 | 21 | protected FileService fileService; 22 | 23 | private JSch jsch; 24 | 25 | private Session session; 26 | 27 | private static final String SFTP_CHANNEL_NAME = "sftp"; 28 | 29 | private static final String CONFIG_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking"; 30 | 31 | private static final String CONFIG_DISABLED = "no"; 32 | 33 | /** 34 | * Constructor. 35 | * 36 | * @param sftpRemoteCopyProperties the properties to use for remote copying 37 | * @param jsch the Jsch dependency used for SSH connections 38 | * @param fileService the file service dependency 39 | */ 40 | @Autowired 41 | public AbstractSFTPRemoteCopy(SFTPRemoteCopyProperties sftpRemoteCopyProperties, JSch jsch, FileService fileService) { 42 | this.sftpRemoteCopyProperties = sftpRemoteCopyProperties; 43 | this.jsch = jsch; 44 | this.fileService = fileService; 45 | } 46 | 47 | /** 48 | * Creates a SFTP channel. 49 | * 50 | * @return SFTP channel 51 | * @throws SFTPRemoteCopyException exception in case of an error 52 | */ 53 | protected ChannelSftp createSFTPChannel() throws SFTPRemoteCopyException { 54 | Channel channel; 55 | try { 56 | session = jsch.getSession(sftpRemoteCopyProperties.getUsername(), sftpRemoteCopyProperties.getHost()); 57 | session.setConfig(CONFIG_STRICT_HOST_KEY_CHECKING, CONFIG_DISABLED); 58 | session.setPassword(sftpRemoteCopyProperties.getPassword()); 59 | session.connect(); 60 | channel = session.openChannel(SFTP_CHANNEL_NAME); 61 | } catch (JSchException exception) { 62 | session.disconnect(); 63 | throw new SFTPRemoteCopyException(String.format("Could not establish SSH connection: %s", 64 | exception.getMessage()), exception); 65 | } 66 | 67 | if (channel == null) { 68 | session.disconnect(); 69 | throw new SFTPRemoteCopyException("No channel was found."); 70 | } 71 | 72 | try { 73 | channel.connect(); 74 | } catch (JSchException exception) { 75 | session.disconnect(); 76 | throw new SFTPRemoteCopyException(String.format("Could not establish SFTP channel: %s", 77 | exception.getMessage()), exception); 78 | } 79 | 80 | if (!(channel instanceof ChannelSftp)) { 81 | channel.disconnect(); 82 | session.disconnect(); 83 | throw new SFTPRemoteCopyException("No SFTP channel was found."); 84 | } 85 | 86 | return (ChannelSftp) channel; 87 | } 88 | 89 | /** 90 | * Disconnect SSH session if existing. 91 | */ 92 | protected void disconnectSession() { 93 | if (session != null) { 94 | session.disconnect(); 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/remotecopy/FTPRemoteCopy.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.remotecopy; //NOSONAR 2 | 3 | import com.github._1element.sc.events.RemoteCopyEvent; 4 | import com.github._1element.sc.exception.FTPRemoteCopyException; 5 | import com.github._1element.sc.properties.FTPRemoteCopyProperties; 6 | import com.github._1element.sc.service.FileService; 7 | 8 | import org.apache.commons.net.ftp.FTPClient; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 13 | import org.springframework.context.annotation.Scope; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.nio.file.Path; 19 | 20 | /** 21 | * Copy surveillance image to FTP remote server (backup). 22 | */ 23 | @ConditionalOnProperty(name = "sc.remotecopy.ftp.enabled", havingValue = "true") 24 | @Component 25 | @Scope("prototype") 26 | public class FTPRemoteCopy extends AbstractFTPRemoteCopy implements RemoteCopy { 27 | 28 | private static final Logger LOG = LoggerFactory.getLogger(FTPRemoteCopy.class); 29 | 30 | @Autowired 31 | public FTPRemoteCopy(FTPRemoteCopyProperties ftpRemoteCopyProperties, FTPClient ftp, FileService fileService) { 32 | super(ftpRemoteCopyProperties, ftp, fileService); 33 | } 34 | 35 | @Override 36 | public void handle(RemoteCopyEvent remoteCopyEvent) { 37 | LOG.debug("FTP remote copy handler for '{}' invoked.", remoteCopyEvent.getFileName()); 38 | 39 | try { 40 | connect(); 41 | transferFile(remoteCopyEvent.getFileName()); 42 | } catch (Exception exception) { 43 | LOG.warn("Error during remote FTP copy: {}", exception.getMessage()); 44 | } finally { 45 | disconnect(); 46 | } 47 | } 48 | 49 | /** 50 | * Transfer file to FTP server. 51 | * 52 | * @param completeLocalFilePath complete path to the local file 53 | * @throws FTPRemoteCopyException exception if file could not be uploaded 54 | * @throws IOException exception in case of an IO error 55 | */ 56 | private void transferFile(String completeLocalFilePath) throws FTPRemoteCopyException, IOException { 57 | Path path = fileService.getPath(completeLocalFilePath); 58 | 59 | try (InputStream inputStream = fileService.createInputStream(path)) { 60 | if (!ftp.storeFile(ftpRemoteCopyProperties.getDir() + path.getFileName().toString(), inputStream)) { 61 | throw new FTPRemoteCopyException(String.format("Could not upload file to remote FTP server. Response was: %s", 62 | ftp.getReplyString())); 63 | } 64 | } 65 | 66 | LOG.info("File '{}' was successfully uploaded to remote FTP server.", path.getFileName().toString()); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/remotecopy/RemoteCopy.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.remotecopy; //NOSONAR 2 | 3 | import com.github._1element.sc.events.RemoteCopyEvent; 4 | import org.springframework.context.event.EventListener; 5 | 6 | /** 7 | * Remote copy interface. 8 | */ 9 | public interface RemoteCopy { 10 | 11 | /** 12 | * Listen to remote copy events and handle copy action. 13 | * 14 | * @param remoteCopyEvent remote copy event 15 | */ 16 | @EventListener 17 | void handle(RemoteCopyEvent remoteCopyEvent); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/remotecopy/RemoteCopyCleanup.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.remotecopy; //NOSONAR 2 | 3 | /** 4 | * Interface for remote copy cleanup components. 5 | */ 6 | public interface RemoteCopyCleanup { 7 | 8 | /** 9 | * Remove old files. Should be scheduled. 10 | */ 11 | void cleanup(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/domain/remotecopy/SFTPRemoteCopy.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.remotecopy; //NOSONAR 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.Path; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.context.annotation.Scope; 12 | import org.springframework.stereotype.Component; 13 | 14 | import com.github._1element.sc.events.RemoteCopyEvent; 15 | import com.github._1element.sc.exception.SFTPRemoteCopyException; 16 | import com.github._1element.sc.properties.SFTPRemoteCopyProperties; 17 | import com.github._1element.sc.service.FileService; 18 | import com.jcraft.jsch.ChannelSftp; 19 | import com.jcraft.jsch.JSch; 20 | import com.jcraft.jsch.SftpException; 21 | 22 | /** 23 | * Copy surveillance image to SFTP remote server (backup). 24 | */ 25 | @ConditionalOnProperty(name = "sc.remotecopy.sftp.enabled", havingValue = "true") 26 | @Component 27 | @Scope("prototype") 28 | public class SFTPRemoteCopy extends AbstractSFTPRemoteCopy implements RemoteCopy { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(SFTPRemoteCopy.class); 31 | 32 | @Autowired 33 | public SFTPRemoteCopy(SFTPRemoteCopyProperties sftpRemoteCopyProperties, JSch jsch, FileService fileService) { 34 | super(sftpRemoteCopyProperties, jsch, fileService); 35 | } 36 | 37 | @Override 38 | public void handle(RemoteCopyEvent remoteCopyEvent) { 39 | LOG.debug("SFTP remote copy handler for '{}' invoked.", remoteCopyEvent.getFileName()); 40 | 41 | ChannelSftp sftpChannel = null; 42 | try { 43 | sftpChannel = createSFTPChannel(); 44 | transferFile(remoteCopyEvent.getFileName(), sftpChannel); 45 | } catch (SFTPRemoteCopyException exception) { 46 | LOG.warn("Error during remote SFTP copy: '{}'", exception.getMessage()); 47 | } finally { 48 | if (sftpChannel != null) { 49 | sftpChannel.disconnect(); 50 | } 51 | disconnectSession(); 52 | } 53 | } 54 | 55 | /** 56 | * Uploads a file using the given SFTP channel. 57 | * 58 | * @param completeLocalFilePath the full path to the local file to upload 59 | * @param sftpChannel the SFTP channel that will be used for the transfer 60 | * @throws SFTPRemoteCopyException exception in case of an error 61 | */ 62 | private void transferFile(String completeLocalFilePath, ChannelSftp sftpChannel) throws SFTPRemoteCopyException { 63 | Path path = fileService.getPath(completeLocalFilePath); 64 | 65 | try (InputStream inputStream = fileService.createInputStream(path)) { 66 | sftpChannel.cd(sftpRemoteCopyProperties.getDir()); 67 | sftpChannel.put(inputStream, path.getFileName().toString()); 68 | } catch (SftpException | IOException exception) { 69 | throw new SFTPRemoteCopyException("Could not upload file to remote SFTP server: " + exception.getMessage(), 70 | exception); 71 | } 72 | 73 | LOG.info("File '{}' was successfully uploaded to remote SFTP server.", path.getFileName().toString()); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/dto/CameraResource.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.dto; //NOSONAR 2 | 3 | /** 4 | * Camera REST resource. 5 | * Similar to {@link com.github._1element.sc.domain.Camera} but with additional/limited attributes. 6 | * We do not want to expose our internal entity so this DTO is used. 7 | */ 8 | public class CameraResource { 9 | 10 | private String id; 11 | 12 | private String name; 13 | 14 | private String snapshotProxyUrl; 15 | 16 | private String streamGeneratorUrl; 17 | 18 | public String getId() { 19 | return id; 20 | } 21 | 22 | public void setId(final String id) { 23 | this.id = id; 24 | } 25 | 26 | public String getName() { 27 | return name; 28 | } 29 | 30 | public void setName(final String name) { 31 | this.name = name; 32 | } 33 | 34 | public String getSnapshotProxyUrl() { 35 | return snapshotProxyUrl; 36 | } 37 | 38 | public void setSnapshotProxyUrl(final String snapshotProxyUrl) { 39 | this.snapshotProxyUrl = snapshotProxyUrl; 40 | } 41 | 42 | public String getStreamGeneratorUrl() { 43 | return streamGeneratorUrl; 44 | } 45 | 46 | public void setStreamGeneratorUrl(final String streamGeneratorUrl) { 47 | this.streamGeneratorUrl = streamGeneratorUrl; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/dto/ImagesCameraSummaryResult.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.dto; //NOSONAR 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import com.github._1element.sc.domain.Camera; 6 | import java.util.Objects; 7 | 8 | /** 9 | * Summary of surveillance images for each camera (count and most recent image date). 10 | */ 11 | public class ImagesCameraSummaryResult { 12 | 13 | private final Camera camera; 14 | 15 | private final Long count; 16 | 17 | private final LocalDateTime mostRecentDate; 18 | 19 | /** 20 | * Constructs a new summary of surveillance images for each camera. 21 | * 22 | * @param camera the camera 23 | * @param count the count of images 24 | * @param mostRecentDate the most recent image date 25 | */ 26 | public ImagesCameraSummaryResult(final Camera camera, final Long count, final LocalDateTime mostRecentDate) { 27 | this.camera = camera; 28 | this.count = count; 29 | this.mostRecentDate = mostRecentDate; 30 | } 31 | 32 | public Camera getCamera() { 33 | return camera; 34 | } 35 | 36 | public Long getCount() { 37 | return count; 38 | } 39 | 40 | public LocalDateTime getMostRecentDate() { 41 | return mostRecentDate; 42 | } 43 | 44 | @Override 45 | public boolean equals(final Object other) { 46 | if (!(other instanceof ImagesCameraSummaryResult)) { 47 | return false; 48 | } 49 | final ImagesCameraSummaryResult castOther = (ImagesCameraSummaryResult) other; 50 | return Objects.equals(camera, castOther.camera) && Objects.equals(count, castOther.count) 51 | && Objects.equals(mostRecentDate, castOther.mostRecentDate); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Objects.hash(camera, count, mostRecentDate); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/dto/PushNotificationSettingResource.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.dto; //NOSONAR 2 | 3 | /** 4 | * Push notification setting resource. 5 | * REST projection for the internal {@link com.github._1element.sc.domain.PushNotificationSetting} entity. 6 | */ 7 | public class PushNotificationSettingResource { 8 | 9 | private final String cameraId; 10 | 11 | private final String cameraName; 12 | 13 | private final boolean enabled; 14 | 15 | /** 16 | * Constructor. 17 | * 18 | * @param cameraId the camera identifier 19 | * @param cameraName the camera name 20 | * @param enabled the push notification status (enabled/disabled) 21 | */ 22 | public PushNotificationSettingResource(final String cameraId, final String cameraName, final boolean enabled) { 23 | this.cameraId = cameraId; 24 | this.cameraName = cameraName; 25 | this.enabled = enabled; 26 | } 27 | 28 | public String getCameraId() { 29 | return cameraId; 30 | } 31 | 32 | public String getCameraName() { 33 | return cameraName; 34 | } 35 | 36 | public boolean isEnabled() { 37 | return enabled; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/dto/PushNotificationSettingUpdateResource.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.dto; //NOSONAR 2 | 3 | /** 4 | * DTO used to update {@link com.github._1element.sc.domain.PushNotificationSetting}. 5 | */ 6 | public class PushNotificationSettingUpdateResource { 7 | 8 | private String cameraId; 9 | 10 | private boolean enabled; 11 | 12 | public String getCameraId() { 13 | return cameraId; 14 | } 15 | 16 | public void setCameraId(final String cameraId) { 17 | this.cameraId = cameraId; 18 | } 19 | 20 | public boolean isEnabled() { 21 | return enabled; 22 | } 23 | 24 | public void setEnabled(final boolean enabled) { 25 | this.enabled = enabled; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/dto/SurveillanceImageBulkUpdateResource.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.dto; //NOSONAR 2 | 3 | import java.time.LocalDateTime; 4 | 5 | /** 6 | * DTO used to bulk update all {@link com.github._1element.sc.domain.SurveillanceImage} before a provided timestamp. 7 | */ 8 | public class SurveillanceImageBulkUpdateResource { 9 | 10 | private LocalDateTime dateBefore; 11 | 12 | private boolean archived; 13 | 14 | public LocalDateTime getDateBefore() { 15 | return dateBefore; 16 | } 17 | 18 | public void setDateBefore(final LocalDateTime dateBefore) { 19 | this.dateBefore = dateBefore; 20 | } 21 | 22 | public boolean isArchived() { 23 | return archived; 24 | } 25 | 26 | public void setArchived(final boolean archived) { 27 | this.archived = archived; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/dto/SurveillanceImageResource.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.dto; //NOSONAR 2 | 3 | import java.time.LocalDateTime; 4 | 5 | /** 6 | * REST projection for the internal {@link com.github._1element.sc.domain.SurveillanceImage} entity. 7 | */ 8 | public class SurveillanceImageResource { 9 | 10 | private long id; 11 | 12 | private String fileName; 13 | 14 | private String cameraId; 15 | 16 | private String cameraName; 17 | 18 | private LocalDateTime receivedAt; 19 | 20 | private boolean archived; 21 | 22 | public long getId() { 23 | return id; 24 | } 25 | 26 | public void setId(final long id) { 27 | this.id = id; 28 | } 29 | 30 | public String getFileName() { 31 | return fileName; 32 | } 33 | 34 | public void setFileName(final String fileName) { 35 | this.fileName = fileName; 36 | } 37 | 38 | public String getCameraId() { 39 | return cameraId; 40 | } 41 | 42 | public void setCameraId(final String cameraId) { 43 | this.cameraId = cameraId; 44 | } 45 | 46 | public String getCameraName() { 47 | return cameraName; 48 | } 49 | 50 | public void setCameraName(final String cameraName) { 51 | this.cameraName = cameraName; 52 | } 53 | 54 | public LocalDateTime getReceivedAt() { 55 | return receivedAt; 56 | } 57 | 58 | public void setReceivedAt(final LocalDateTime receivedAt) { 59 | this.receivedAt = receivedAt; 60 | } 61 | 62 | public boolean isArchived() { 63 | return archived; 64 | } 65 | 66 | public void setArchived(final boolean archived) { 67 | this.archived = archived; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/dto/SurveillanceImageUpdateResource.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.dto; //NOSONAR 2 | 3 | /** 4 | * DTO used to update {@link com.github._1element.sc.domain.SurveillanceImage}. 5 | */ 6 | public class SurveillanceImageUpdateResource { 7 | 8 | private long id; 9 | 10 | private boolean archived; 11 | 12 | public long getId() { 13 | return id; 14 | } 15 | 16 | public void setId(final long id) { 17 | this.id = id; 18 | } 19 | 20 | public boolean isArchived() { 21 | return archived; 22 | } 23 | 24 | public void setArchived(final boolean archived) { 25 | this.archived = archived; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/events/ImageReceivedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.events; //NOSONAR 2 | 3 | import com.github._1element.sc.domain.Camera; 4 | 5 | /** 6 | * Event for received image. 7 | */ 8 | public class ImageReceivedEvent { 9 | 10 | private byte[] image; 11 | 12 | private Camera source; 13 | 14 | public ImageReceivedEvent(byte[] image, Camera source) { 15 | this.image = image; 16 | this.source = source; 17 | } 18 | 19 | public byte[] getImage() { 20 | return image; 21 | } 22 | 23 | public Camera getSource() { 24 | return source; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/events/PushNotificationEvent.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.events; //NOSONAR 2 | 3 | import com.github._1element.sc.domain.Camera; 4 | 5 | /** 6 | * Event to trigger push notification. 7 | */ 8 | public class PushNotificationEvent { 9 | 10 | private Camera camera; 11 | 12 | public PushNotificationEvent(Camera camera) { 13 | this.camera = camera; 14 | } 15 | 16 | public Camera getCamera() { 17 | return camera; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/events/RemoteCopyEvent.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.events; //NOSONAR 2 | 3 | /** 4 | * Event to trigger remote copy. 5 | */ 6 | public class RemoteCopyEvent { 7 | 8 | private String fileName; 9 | 10 | public RemoteCopyEvent(String fileName) { 11 | this.fileName = fileName; 12 | } 13 | 14 | public String getFileName() { 15 | return fileName; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/exception/CameraNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.exception; //NOSONAR 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 7 | public class CameraNotFoundException extends Exception { 8 | 9 | private static final String MESSAGE_CAMERA_NOT_FOUND = "Camera not found."; 10 | 11 | public CameraNotFoundException() { 12 | super(MESSAGE_CAMERA_NOT_FOUND); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/exception/FTPRemoteCopyException.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.exception; //NOSONAR 2 | 3 | public class FTPRemoteCopyException extends Exception { 4 | 5 | public FTPRemoteCopyException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/exception/ForbiddenException.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.exception; //NOSONAR 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.FORBIDDEN) 7 | public class ForbiddenException extends Exception { 8 | 9 | public ForbiddenException(String message) { 10 | super(message); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/exception/PropertyNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.exception; //NOSONAR 2 | 3 | public class PropertyNotFoundException extends Exception { 4 | 5 | public PropertyNotFoundException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/exception/ProxyException.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.exception; //NOSONAR 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) 7 | public class ProxyException extends Exception { 8 | 9 | public ProxyException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/exception/PushNotificationClientException.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.exception; //NOSONAR 2 | 3 | public class PushNotificationClientException extends Exception { 4 | 5 | private static final String MESSAGE = "Push notification could not be delivered to external service."; 6 | 7 | public PushNotificationClientException(Throwable cause) { 8 | super(MESSAGE, cause); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/exception/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.exception; //NOSONAR 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 7 | public class ResourceNotFoundException extends Exception { 8 | 9 | public ResourceNotFoundException(String message) { 10 | super(message); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/exception/SFTPRemoteCopyException.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.exception; //NOSONAR 2 | 3 | public class SFTPRemoteCopyException extends Exception { 4 | 5 | public SFTPRemoteCopyException(String message) { 6 | super(message); 7 | } 8 | 9 | public SFTPRemoteCopyException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/FTPRemoteCopyProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * Configuration properties for FTP remote copy. 9 | */ 10 | @Component 11 | @ConfigurationProperties("sc.remotecopy.ftp") 12 | public class FTPRemoteCopyProperties { 13 | 14 | private boolean enabled; 15 | 16 | private String host; 17 | 18 | private String dir = "/"; 19 | 20 | private String username; 21 | 22 | private String password; 23 | 24 | private boolean cleanupEnabled = false; 25 | 26 | private long cleanupMaxDiskSpace; 27 | 28 | private int cleanupKeep; 29 | 30 | public boolean isEnabled() { 31 | return enabled; 32 | } 33 | 34 | public void setEnabled(final boolean enabled) { 35 | this.enabled = enabled; 36 | } 37 | 38 | public String getHost() { 39 | return host; 40 | } 41 | 42 | public void setHost(final String host) { 43 | this.host = host; 44 | } 45 | 46 | public String getDir() { 47 | return dir; 48 | } 49 | 50 | public void setDir(final String dir) { 51 | this.dir = dir; 52 | } 53 | 54 | public String getUsername() { 55 | return username; 56 | } 57 | 58 | public void setUsername(final String username) { 59 | this.username = username; 60 | } 61 | 62 | public String getPassword() { 63 | return password; 64 | } 65 | 66 | public void setPassword(final String password) { 67 | this.password = password; 68 | } 69 | 70 | public boolean isCleanupEnabled() { 71 | return cleanupEnabled; 72 | } 73 | 74 | public void setCleanupEnabled(final boolean cleanupEnabled) { 75 | this.cleanupEnabled = cleanupEnabled; 76 | } 77 | 78 | public long getCleanupMaxDiskSpace() { 79 | return cleanupMaxDiskSpace; 80 | } 81 | 82 | public void setCleanupMaxDiskSpace(final long cleanupMaxDiskSpace) { 83 | this.cleanupMaxDiskSpace = FileUtils.ONE_MB * cleanupMaxDiskSpace; 84 | } 85 | 86 | public int getCleanupKeep() { 87 | return cleanupKeep; 88 | } 89 | 90 | public void setCleanupKeep(final int cleanupKeep) { 91 | this.cleanupKeep = cleanupKeep; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/FtpProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * FTP server specific configuration properties. 8 | */ 9 | @Component 10 | @ConfigurationProperties("sc.ftp") 11 | public class FtpProperties { 12 | 13 | private boolean enabled = false; 14 | 15 | private int port = 2121; 16 | 17 | public boolean isEnabled() { 18 | return enabled; 19 | } 20 | 21 | public void setEnabled(final boolean enabled) { 22 | this.enabled = enabled; 23 | } 24 | 25 | public int getPort() { 26 | return port; 27 | } 28 | 29 | public void setPort(final int port) { 30 | this.port = port; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/ImageProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * Image specific configuration properties. 8 | */ 9 | @Component 10 | @ConfigurationProperties("sc.image") 11 | public class ImageProperties { 12 | 13 | private String storageDir; 14 | 15 | private String[] validExtensions; 16 | 17 | private int pageSize = 100; 18 | 19 | public String getStorageDir() { 20 | return storageDir; 21 | } 22 | 23 | public void setStorageDir(final String storageDir) { 24 | this.storageDir = storageDir; 25 | } 26 | 27 | public String[] getValidExtensions() { 28 | return validExtensions; 29 | } 30 | 31 | public void setValidExtensions(final String[] validExtensions) { 32 | this.validExtensions = validExtensions; 33 | } 34 | 35 | public int getPageSize() { 36 | return pageSize; 37 | } 38 | 39 | public void setPageSize(final int pageSize) { 40 | this.pageSize = pageSize; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/ImageThumbnailProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * Image thumbnail specific configuration properties. 8 | */ 9 | @Component 10 | @ConfigurationProperties("sc.image.thumbnail") 11 | public class ImageThumbnailProperties { 12 | 13 | private int width = 200; 14 | 15 | private int height = 200; 16 | 17 | private double quality = 0.8; 18 | 19 | public int getWidth() { 20 | return width; 21 | } 22 | 23 | public void setWidth(final int width) { 24 | this.width = width; 25 | } 26 | 27 | public int getHeight() { 28 | return height; 29 | } 30 | 31 | public void setHeight(final int height) { 32 | this.height = height; 33 | } 34 | 35 | public double getQuality() { 36 | return quality; 37 | } 38 | 39 | public void setQuality(final double quality) { 40 | this.quality = quality; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/MqttProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * MQTT specific configuration properties. 8 | */ 9 | @Component 10 | @ConfigurationProperties("sc.mqtt") 11 | public class MqttProperties { 12 | 13 | private boolean enabled = false; 14 | 15 | private String brokerConnection; 16 | 17 | private String topicFilter; 18 | 19 | private String username; 20 | 21 | private String password; 22 | 23 | public boolean isEnabled() { 24 | return enabled; 25 | } 26 | 27 | public void setEnabled(final boolean enabled) { 28 | this.enabled = enabled; 29 | } 30 | 31 | public String getBrokerConnection() { 32 | return brokerConnection; 33 | } 34 | 35 | public void setBrokerConnection(final String brokerConnection) { 36 | this.brokerConnection = brokerConnection; 37 | } 38 | 39 | public String getTopicFilter() { 40 | return topicFilter; 41 | } 42 | 43 | public void setTopicFilter(final String topicFilter) { 44 | this.topicFilter = topicFilter; 45 | } 46 | 47 | public String getUsername() { 48 | return username; 49 | } 50 | 51 | public void setUsername(final String username) { 52 | this.username = username; 53 | } 54 | 55 | public String getPassword() { 56 | return password; 57 | } 58 | 59 | public void setPassword(final String password) { 60 | this.password = password; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/MultiCameraAwareProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import com.github._1element.sc.exception.PropertyNotFoundException; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.core.env.Environment; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * Properties handling for multi camera. 11 | */ 12 | @Component 13 | public class MultiCameraAwareProperties { 14 | 15 | private final Environment environment; 16 | 17 | public static final String PROPERTY_MULTI_CAMERA_PREFIX = "sc.camera[%s]."; 18 | 19 | @Autowired 20 | public MultiCameraAwareProperties(final Environment environment) { 21 | this.environment = environment; 22 | } 23 | 24 | /** 25 | * Returns property value for the provided key and camera id. 26 | * 27 | * @param propertyKey the property key to resolve 28 | * @param cameraId the camera id to use for the property key building 29 | * @param targetType the expected type of the property value 30 | * 31 | * @return property value 32 | * @throws PropertyNotFoundException exception if property was not found 33 | */ 34 | public T getProperty(final String propertyKey, final String cameraId, final Class targetType) 35 | throws PropertyNotFoundException { 36 | if (StringUtils.isBlank(cameraId)) { 37 | throw new PropertyNotFoundException(String.format("Property '%s' not found. Empty camera id was given.", 38 | propertyKey)); 39 | } 40 | 41 | final String formattedPropertyKey = String.format(propertyKey, cameraId); 42 | 43 | if (!environment.containsProperty(formattedPropertyKey)) { 44 | throw new PropertyNotFoundException(String.format("Property not found for key '%s'.", formattedPropertyKey)); 45 | } 46 | 47 | return environment.getProperty(formattedPropertyKey, targetType); 48 | } 49 | 50 | /** 51 | * Returns property value for the provided key and camera id. Default value if none found. 52 | * 53 | * @param propertyKey the property key to resolve 54 | * @param cameraId the camera id to use for the property key building 55 | * @param targetType the expected type of the property value 56 | * @param defaultValue the default value if none found 57 | * 58 | * @return property value 59 | */ 60 | public T getProperty(final String propertyKey, final String cameraId, final Class targetType, 61 | final T defaultValue) { 62 | try { 63 | return getProperty(propertyKey, cameraId, targetType); 64 | } catch (final Exception exception) { 65 | return defaultValue; 66 | } 67 | } 68 | 69 | /** 70 | * Returns string property value for given key and camera id. 71 | * 72 | * @param propertyKey the property key to resolve 73 | * @param cameraId the camera id to use for the property key building 74 | * 75 | * @return property value 76 | * @throws PropertyNotFoundException exception if property was not found 77 | */ 78 | public String getProperty(final String propertyKey, final String cameraId) throws PropertyNotFoundException { 79 | return getProperty(propertyKey, cameraId, String.class); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/PushNotificationProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * Push notification properties. 8 | */ 9 | @Component 10 | @ConfigurationProperties("sc.pushnotification") 11 | public class PushNotificationProperties { 12 | 13 | private boolean enabled = false; 14 | 15 | private String adapter; 16 | 17 | private String apiToken; 18 | 19 | private String userToken; 20 | 21 | private long groupTime = 0; 22 | 23 | private String url; 24 | 25 | public boolean isEnabled() { 26 | return enabled; 27 | } 28 | 29 | public void setEnabled(final boolean enabled) { 30 | this.enabled = enabled; 31 | } 32 | 33 | public String getAdapter() { 34 | return adapter; 35 | } 36 | 37 | public void setAdapter(final String adapter) { 38 | this.adapter = adapter; 39 | } 40 | 41 | public String getApiToken() { 42 | return apiToken; 43 | } 44 | 45 | public void setApiToken(final String apiToken) { 46 | this.apiToken = apiToken; 47 | } 48 | 49 | public String getUserToken() { 50 | return userToken; 51 | } 52 | 53 | public void setUserToken(final String userToken) { 54 | this.userToken = userToken; 55 | } 56 | 57 | public long getGroupTime() { 58 | return groupTime; 59 | } 60 | 61 | public void setGroupTime(final long groupTime) { 62 | this.groupTime = groupTime; 63 | } 64 | 65 | public String getUrl() { 66 | return url; 67 | } 68 | 69 | public void setUrl(final String url) { 70 | this.url = url; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/SFTPRemoteCopyProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * Configuration properties for SFTP remote copy. 9 | */ 10 | @Component 11 | @ConfigurationProperties("sc.remotecopy.sftp") 12 | public class SFTPRemoteCopyProperties { 13 | 14 | private boolean enabled; 15 | 16 | private String host; 17 | 18 | private String dir = "/"; 19 | 20 | private String username; 21 | 22 | private String password; 23 | 24 | private boolean cleanupEnabled = false; 25 | 26 | private long cleanupMaxDiskSpace; 27 | 28 | private int cleanupKeep; 29 | 30 | public boolean isEnabled() { 31 | return enabled; 32 | } 33 | 34 | public void setEnabled(final boolean enabled) { 35 | this.enabled = enabled; 36 | } 37 | 38 | public String getHost() { 39 | return host; 40 | } 41 | 42 | public void setHost(final String host) { 43 | this.host = host; 44 | } 45 | 46 | public String getDir() { 47 | return dir; 48 | } 49 | 50 | public void setDir(final String dir) { 51 | this.dir = dir; 52 | } 53 | 54 | public String getUsername() { 55 | return username; 56 | } 57 | 58 | public void setUsername(final String username) { 59 | this.username = username; 60 | } 61 | 62 | public String getPassword() { 63 | return password; 64 | } 65 | 66 | public void setPassword(final String password) { 67 | this.password = password; 68 | } 69 | 70 | public boolean isCleanupEnabled() { 71 | return cleanupEnabled; 72 | } 73 | 74 | public void setCleanupEnabled(final boolean cleanupEnabled) { 75 | this.cleanupEnabled = cleanupEnabled; 76 | } 77 | 78 | public long getCleanupMaxDiskSpace() { 79 | return cleanupMaxDiskSpace; 80 | } 81 | 82 | public void setCleanupMaxDiskSpace(final long cleanupMaxDiskSpace) { 83 | this.cleanupMaxDiskSpace = FileUtils.ONE_MB * cleanupMaxDiskSpace; 84 | } 85 | 86 | public int getCleanupKeep() { 87 | return cleanupKeep; 88 | } 89 | 90 | public void setCleanupKeep(final int cleanupKeep) { 91 | this.cleanupKeep = cleanupKeep; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/StreamGenerationProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | @ConfigurationProperties("sc.stream-generation") 8 | public class StreamGenerationProperties { 9 | 10 | private int mjpegDelay = 500; 11 | 12 | public int getMjpegDelay() { 13 | return mjpegDelay; 14 | } 15 | 16 | public void setMjpegDelay(final int mjpegDelay) { 17 | this.mjpegDelay = mjpegDelay; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/properties/SurveillanceSecurityProperties.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; //NOSONAR 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * Configuration properties for handling authorization. 8 | */ 9 | @Component 10 | @ConfigurationProperties("sc.security") 11 | public class SurveillanceSecurityProperties { 12 | 13 | private String username; 14 | 15 | private String password; 16 | 17 | private String secret; 18 | 19 | private String cookieName = "JWT"; 20 | 21 | private int tokenExpiration = 2592000; 22 | 23 | public String getUsername() { 24 | return username; 25 | } 26 | 27 | public void setUsername(final String username) { 28 | this.username = username; 29 | } 30 | 31 | public String getPassword() { 32 | return password; 33 | } 34 | 35 | public void setPassword(final String password) { 36 | this.password = password; 37 | } 38 | 39 | public String getSecret() { 40 | return secret; 41 | } 42 | 43 | public void setSecret(final String secret) { 44 | this.secret = secret; 45 | } 46 | 47 | public String getCookieName() { 48 | return cookieName; 49 | } 50 | 51 | public void setCookieName(final String cookieName) { 52 | this.cookieName = cookieName; 53 | } 54 | 55 | public int getTokenExpiration() { 56 | return tokenExpiration; 57 | } 58 | 59 | public void setTokenExpiration(final int tokenExpiration) { 60 | this.tokenExpiration = tokenExpiration; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/repository/PushNotificationSettingRepository.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.repository; //NOSONAR 2 | 3 | import com.github._1element.sc.domain.PushNotificationSetting; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | /** 7 | * Repository for push notification settings. 8 | */ 9 | public interface PushNotificationSettingRepository extends JpaRepository { 10 | 11 | PushNotificationSetting findByCameraId(String cameraId); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/repository/SurveillanceImageRepository.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.repository; //NOSONAR 2 | 3 | import com.github._1element.sc.domain.SurveillanceImage; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Modifying; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.time.LocalDateTime; 13 | import java.util.List; 14 | 15 | /** 16 | * Surveillance image repository. 17 | */ 18 | public interface SurveillanceImageRepository extends JpaRepository { 19 | 20 | Page findAllByArchived(boolean archived, Pageable pageable); 21 | 22 | Page findAllByCameraIdAndArchived(String cameraId, boolean archived, Pageable pageable); 23 | 24 | @Query("select s from SurveillanceImage s where (s.receivedAt between :start and :end) and s.archived = :archived") 25 | Page findAllForDateRange(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end, 26 | @Param("archived") boolean archived, Pageable pageable); 27 | 28 | @Query("select s from SurveillanceImage s where s.cameraId = :cameraId and (s.receivedAt between :start and :end) " 29 | + "and s.archived = :archived") 30 | Page findAllForDateRangeAndCameraId(@Param("start") LocalDateTime start, 31 | @Param("end") LocalDateTime end, 32 | @Param("cameraId") String cameraId, 33 | @Param("archived") boolean archived, Pageable pageable); 34 | 35 | @Query("select count(*) from SurveillanceImage s where s.cameraId = :cameraId and s.archived = false") 36 | Long countImagesForCamera(@Param("cameraId") String cameraId); 37 | 38 | @Query("select s.receivedAt from SurveillanceImage s where s.cameraId = :cameraId and s.archived = false " 39 | + "order by s.receivedAt desc") 40 | List getMostRecentImageDateForCamera(@Param("cameraId") String cameraId, Pageable pageable); 41 | 42 | @Query("select s from SurveillanceImage s where s.archived = true and s.receivedAt <= :dateBefore") 43 | List getArchivedImagesToCleanup(@Param("dateBefore") LocalDateTime before); 44 | 45 | @Modifying 46 | @Transactional 47 | @Query("update SurveillanceImage s set s.archived = true where s.id in :ids") 48 | void updateSetArchived(@Param("ids") List ids); 49 | 50 | @Modifying 51 | @Transactional 52 | @Query("update SurveillanceImage s set s.archived = :value where s.receivedAt <= :dateBefore") 53 | void updateArchiveState(@Param("value") boolean value, @Param("dateBefore") LocalDateTime before); 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/security/JwtAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.security; //NOSONAR 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.AuthenticationEntryPoint; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | 11 | /** 12 | * Authentication entry point. 13 | * This will respond to all unauthorized requests with a 401 header. 14 | */ 15 | @Component 16 | public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { 17 | 18 | @Override 19 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) 20 | throws IOException { 21 | 22 | // This is invoked when a user tries to access a secured REST resource without supplying any credentials. 23 | // We just send a 401 unauthorized response. 24 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/security/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.security; //NOSONAR 2 | 3 | import com.github._1element.sc.service.JwtAuthenticationService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.web.filter.OncePerRequestFilter; 8 | 9 | import javax.servlet.FilterChain; 10 | import javax.servlet.ServletException; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | 15 | /** 16 | * Authentication filter, will be passed through for all requests. 17 | * If cookie with valid token exists authentication will be passed to the security context holder. 18 | */ 19 | public class JwtAuthenticationFilter extends OncePerRequestFilter { 20 | 21 | @Autowired 22 | private JwtAuthenticationService jwtAuthenticationService; 23 | 24 | @Override 25 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 26 | throws IOException, ServletException { 27 | 28 | // gets valid authentication if cookie with token (JWT) exists 29 | Authentication authentication = jwtAuthenticationService.getAuthentication(request); 30 | SecurityContextHolder.getContext().setAuthentication(authentication); 31 | 32 | chain.doFilter(request, response); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/security/JwtAuthenticationRequest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.security; //NOSONAR 2 | 3 | /** 4 | * Authentication request with username and password. 5 | */ 6 | public class JwtAuthenticationRequest { 7 | 8 | private String username; 9 | private String password; 10 | 11 | protected JwtAuthenticationRequest() { 12 | } 13 | 14 | public JwtAuthenticationRequest(String username, String password) { 15 | this.username = username; 16 | this.password = password; 17 | } 18 | 19 | public String getUsername() { 20 | return this.username; 21 | } 22 | 23 | public String getPassword() { 24 | return this.password; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/service/CleanupTasks.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; //NOSONAR 2 | 3 | import com.github._1element.sc.domain.SurveillanceImage; 4 | import com.github._1element.sc.properties.ImageProperties; 5 | import com.github._1element.sc.repository.SurveillanceImageRepository; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.scheduling.annotation.Scheduled; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.io.IOException; 14 | import java.nio.file.Path; 15 | import java.time.LocalDateTime; 16 | import java.util.List; 17 | 18 | /** 19 | * Scheduler component to perform clean up tasks. 20 | */ 21 | @Component 22 | public class CleanupTasks { 23 | 24 | private final SurveillanceImageRepository imageRepository; 25 | 26 | private final FileService fileService; 27 | 28 | private final ImageProperties imageProperties; 29 | 30 | @Value("${sc.archive.cleanup.enabled:false}") 31 | private Boolean isCleanupEnabled; 32 | 33 | @Value("${sc.archive.cleanup.keep:72}") 34 | private Integer keepHours; 35 | 36 | private static final String THUMBNAIL_PREFIX = "thumbnail."; 37 | 38 | private static final String CRON_EVERY_DAY_AT_4_AM = "0 0 4 * * *"; 39 | 40 | private static final Logger LOG = LoggerFactory.getLogger(CleanupTasks.class); 41 | 42 | /** 43 | * Constructor. 44 | * 45 | * @param imageRepository the surveillance image repository 46 | * @param fileService the file service dependency 47 | * @param imageProperties the configured image properties 48 | */ 49 | @Autowired 50 | public CleanupTasks(final SurveillanceImageRepository imageRepository, final FileService fileService, 51 | final ImageProperties imageProperties) { 52 | this.imageRepository = imageRepository; 53 | this.fileService = fileService; 54 | this.imageProperties = imageProperties; 55 | } 56 | 57 | /** 58 | * Remove archived images older than X hours. 59 | */ 60 | @Scheduled(cron = CRON_EVERY_DAY_AT_4_AM) 61 | public void cleanupArchive() { 62 | if (!Boolean.TRUE.equals(isCleanupEnabled)) { 63 | LOG.info("Task to remove old archived images not enabled in configuration. Do nothing."); 64 | return; 65 | } 66 | 67 | final LocalDateTime removeBefore = LocalDateTime.now().minusHours(keepHours); 68 | final List images = imageRepository.getArchivedImagesToCleanup(removeBefore); 69 | 70 | int numberOfImages = 0; 71 | for (final SurveillanceImage image : images) { 72 | final Path imageFilePath = fileService.getPath(imageProperties.getStorageDir() + image.getFileName()); 73 | final Path thumbnailFilePath = fileService.getPath(imageProperties.getStorageDir() + THUMBNAIL_PREFIX 74 | + image.getFileName()); 75 | try { 76 | fileService.delete(imageFilePath); 77 | fileService.delete(thumbnailFilePath); 78 | numberOfImages++; 79 | } catch (final IOException exception) { 80 | LOG.warn("Exception occurred while removing old archived image/thumbnail '{}'/'{}', cause '{}'", 81 | imageFilePath.toString(), thumbnailFilePath.toString(), exception.getMessage()); 82 | } 83 | imageRepository.delete(image); 84 | } 85 | 86 | LOG.info("Successfully removed {} archived images.", numberOfImages); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/service/JwtUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; //NOSONAR 2 | 3 | import com.github._1element.sc.properties.SurveillanceSecurityProperties; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.security.core.userdetails.User; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.Objects; 12 | 13 | /** 14 | * User details service class. 15 | */ 16 | @Service 17 | public class JwtUserDetailsService implements UserDetailsService { 18 | 19 | private final SurveillanceSecurityProperties securityProperties; 20 | 21 | @Autowired 22 | public JwtUserDetailsService(final SurveillanceSecurityProperties securityProperties) { 23 | this.securityProperties = securityProperties; 24 | } 25 | 26 | /** 27 | * Locates the user based on the username. 28 | * There is currently only one single user defined in the application.properties file. 29 | * 30 | * @param username the username to load 31 | * @return a fully populated user record 32 | * @throws UsernameNotFoundException if user was not found 33 | */ 34 | @Override 35 | public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { 36 | Objects.requireNonNull(securityProperties.getUsername(), "Username must not be null. Check your configuration."); 37 | Objects.requireNonNull(securityProperties.getPassword(), "Password must not be null. Check your configuration."); 38 | Objects.requireNonNull(username, "Provided username must not be null."); 39 | 40 | if (securityProperties.getUsername().equals(username)) { 41 | return User.withUsername(securityProperties.getUsername()).password(securityProperties.getPassword()) 42 | .roles("USER").build(); 43 | } 44 | 45 | throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username)); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/service/MjpegGenerationService.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; //NOSONAR 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | import com.github._1element.sc.properties.StreamGenerationProperties; 10 | import com.github._1element.sc.utils.RestTemplateUtils; 11 | import org.springframework.boot.web.client.RestTemplateBuilder; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.web.client.RestClientException; 14 | import org.springframework.web.client.RestTemplate; 15 | 16 | /** 17 | * Generation service to create MJPEG streams. 18 | */ 19 | @Service 20 | public class MjpegGenerationService { 21 | 22 | private final StreamGenerationProperties streamGenerationProperties; 23 | 24 | private final RestTemplateBuilder restTemplateBuilder; 25 | 26 | private static final String NL = "\r\n"; 27 | 28 | private static final String MJPEG_BOUNDARY = "--BoundaryString"; 29 | 30 | private static final String MJPEG_HEAD = MJPEG_BOUNDARY + NL + "Content-type: image/jpeg" + NL + "Content-Length: "; 31 | 32 | public MjpegGenerationService(final StreamGenerationProperties streamGenerationProperties, 33 | final RestTemplateBuilder restTemplateBuilder) { 34 | this.streamGenerationProperties = streamGenerationProperties; 35 | this.restTemplateBuilder = restTemplateBuilder; 36 | } 37 | 38 | /** 39 | * Continuously retrieve JPEG image from given camera snapshotUrl and 40 | * output result as MJPEG stream to the provided HttpServlet response. 41 | * 42 | * @param snapshotUrl the camera snapshot URL to retrieve image from 43 | * @param response the HTTP response to write to 44 | * @throws IOException if output stream could not be written (e.g. client disconnects) 45 | * @throws RestClientException if an HTTP error occurred while accessing the snapshot URL 46 | */ 47 | public void writeSnapshotToOutputStream(final String snapshotUrl, final HttpServletResponse response) 48 | throws IOException { 49 | final RestTemplate restTemplate = RestTemplateUtils.buildWithAuth(restTemplateBuilder, snapshotUrl); 50 | 51 | final OutputStream outputStream = response.getOutputStream(); 52 | 53 | while (!Thread.currentThread().isInterrupted()) { 54 | final byte[] imageData = restTemplate.getForObject(snapshotUrl, byte[].class); 55 | 56 | outputStream.write((MJPEG_HEAD + imageData.length + NL + NL).getBytes()); 57 | outputStream.write(imageData); 58 | outputStream.write((NL + NL).getBytes()); 59 | outputStream.flush(); 60 | 61 | try { 62 | TimeUnit.MILLISECONDS.sleep(streamGenerationProperties.getMjpegDelay()); 63 | } catch (final InterruptedException exception) { 64 | Thread.currentThread().interrupt(); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Set response content type. 71 | * 72 | * @param response HTTP response to set header 73 | */ 74 | public void setContentType(final HttpServletResponse response) { 75 | response.setContentType("multipart/x-mixed-replace; boundary=" + MJPEG_BOUNDARY); 76 | } 77 | 78 | /** 79 | * Set response cache control header. 80 | * 81 | * @param response HTTP response to set header 82 | */ 83 | public void setCacheControlHeader(final HttpServletResponse response) { 84 | response.setHeader("Cache-Control", "no-cache, private"); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/service/ModelMappingService.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; //NOSONAR 2 | 3 | import com.github._1element.sc.controller.SurveillanceProxyController; 4 | import com.github._1element.sc.controller.SurveillanceStreamGenerationController; 5 | import com.github._1element.sc.domain.Camera; 6 | import com.github._1element.sc.domain.SurveillanceImage; 7 | import com.github._1element.sc.dto.CameraResource; 8 | import com.github._1element.sc.dto.SurveillanceImageResource; 9 | import com.github._1element.sc.exception.CameraNotFoundException; 10 | import com.github._1element.sc.exception.ProxyException; 11 | import com.github._1element.sc.repository.CameraRepository; 12 | import com.github._1element.sc.utils.URIConstants; 13 | import org.modelmapper.ModelMapper; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.hateoas.mvc.ControllerLinkBuilder; 18 | import org.springframework.stereotype.Service; 19 | 20 | /** 21 | * Service to convert between internal entities and data transfer objects (DTOs). 22 | * This way we do not expose our internal domain objects/entities. 23 | */ 24 | @Service 25 | public class ModelMappingService { 26 | 27 | private final ModelMapper modelMapper; 28 | 29 | private final CameraRepository cameraRepository; 30 | 31 | private static final Logger LOG = LoggerFactory.getLogger(ModelMappingService.class); 32 | 33 | @Autowired 34 | public ModelMappingService(final ModelMapper modelMapper, final CameraRepository cameraRepository) { 35 | this.modelMapper = modelMapper; 36 | this.cameraRepository = cameraRepository; 37 | } 38 | 39 | /** 40 | * Converts the provided {@link Camera} to a {@link CameraResource} with additional attributes. 41 | * 42 | * @param camera the camera to convert 43 | * @return converted camera resource 44 | */ 45 | public CameraResource convertCameraToResource(final Camera camera) { 46 | final CameraResource cameraResource = modelMapper.map(camera, CameraResource.class); 47 | 48 | String snapshotProxyUrl = null; 49 | try { 50 | snapshotProxyUrl = ControllerLinkBuilder.linkTo(ControllerLinkBuilder 51 | .methodOn(SurveillanceProxyController.class).retrieveSnapshot(camera.getId())).toString(); 52 | } catch (CameraNotFoundException | ProxyException exception) { 53 | LOG.debug("Exception occurred during link building: '{}'", exception.getMessage()); 54 | } 55 | // methodOn() does not work because of void return type 56 | final String streamGeneratorUrl = ControllerLinkBuilder.linkTo(SurveillanceStreamGenerationController.class) 57 | .slash(URIConstants.GENERATE_MJPEG.replace("{id}", camera.getId())).toString(); 58 | 59 | cameraResource.setSnapshotProxyUrl(snapshotProxyUrl); 60 | cameraResource.setStreamGeneratorUrl(streamGeneratorUrl); 61 | 62 | return cameraResource; 63 | } 64 | 65 | /** 66 | * Converts the provided {@link SurveillanceImage} to a {@link SurveillanceImageResource}. 67 | * 68 | * @param surveillanceImage the surveillance image to convert 69 | * @return converted surveillance image resource 70 | */ 71 | public SurveillanceImageResource convertSurveillanceImageToResource(final SurveillanceImage surveillanceImage) { 72 | final SurveillanceImageResource surveillanceImageResource = 73 | modelMapper.map(surveillanceImage, SurveillanceImageResource.class); 74 | 75 | // add camera name 76 | final Camera camera = cameraRepository.findById(surveillanceImage.getCameraId()); 77 | if (camera != null) { 78 | surveillanceImageResource.setCameraName(camera.getName()); 79 | } 80 | 81 | return surveillanceImageResource; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/service/SurveillanceProxyService.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; //NOSONAR 2 | 3 | import com.github._1element.sc.exception.ProxyException; 4 | import com.github._1element.sc.utils.RestTemplateUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.web.client.RestTemplateBuilder; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.client.RestClientException; 12 | import org.springframework.web.client.RestTemplate; 13 | 14 | /** 15 | * Simple proxy service class. 16 | */ 17 | @Service 18 | public class SurveillanceProxyService { 19 | 20 | private final RestTemplateBuilder restTemplateBuilder; 21 | 22 | private static final Logger LOG = LoggerFactory.getLogger(SurveillanceProxyService.class); 23 | 24 | @Autowired 25 | public SurveillanceProxyService(final RestTemplateBuilder restTemplateBuilder) { 26 | this.restTemplateBuilder = restTemplateBuilder; 27 | } 28 | 29 | /** 30 | * Retrieve the provided url (snapshot). 31 | * 32 | * @param url the url to retrieve 33 | * @return the image response 34 | */ 35 | public ResponseEntity retrieveImage(final String url) throws ProxyException { 36 | final RestTemplate restTemplate = RestTemplateUtils.buildWithAuth(restTemplateBuilder, url); 37 | 38 | ResponseEntity response = null; 39 | try { 40 | response = restTemplate.getForEntity(url, byte[].class); 41 | } catch (final RestClientException exception) { 42 | LOG.debug("Could not retrieve snapshot for '{}': '{}'", url, exception.getMessage()); 43 | throw new ProxyException(exception); 44 | } 45 | 46 | return response; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/service/ThumbnailService.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; //NOSONAR 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import com.github._1element.sc.properties.ImageThumbnailProperties; 12 | 13 | import net.coobird.thumbnailator.Thumbnails; 14 | import net.coobird.thumbnailator.name.Rename; 15 | 16 | /** 17 | * Thumbnail service. 18 | */ 19 | @Service 20 | public class ThumbnailService { 21 | 22 | private final ImageThumbnailProperties imageThumbnailProperties; 23 | 24 | private static final Logger LOG = LoggerFactory.getLogger(ThumbnailService.class); 25 | 26 | @Autowired 27 | public ThumbnailService(final ImageThumbnailProperties imageThumbnailProperties) { 28 | this.imageThumbnailProperties = imageThumbnailProperties; 29 | } 30 | 31 | /** 32 | * Generates a thumbnail for the given path. 33 | * 34 | * @param path the file path to create a thumbnail for 35 | */ 36 | public void createThumbnail(final Path path) { 37 | try { 38 | Thumbnails.of(path.toFile()) 39 | .size(imageThumbnailProperties.getWidth(), imageThumbnailProperties.getHeight()) 40 | .outputQuality(imageThumbnailProperties.getQuality()) 41 | .toFiles(Rename.PREFIX_DOT_THUMBNAIL); 42 | } catch (final IOException exception) { 43 | LOG.warn("Unable to generate thumbnail: {}", exception.getMessage()); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/utils/RestTemplateUtils.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.utils; //NOSONAR 2 | 3 | import com.google.common.base.Splitter; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.web.client.RestTemplateBuilder; 7 | import org.springframework.web.client.RestTemplate; 8 | import org.springframework.web.util.UriComponents; 9 | import org.springframework.web.util.UriComponentsBuilder; 10 | 11 | import java.util.List; 12 | 13 | public final class RestTemplateUtils { 14 | 15 | private static final Logger LOG = LoggerFactory.getLogger(RestTemplateUtils.class); 16 | 17 | private RestTemplateUtils() { 18 | // hide constructor for static utility class 19 | } 20 | 21 | /** 22 | * Builds a RestTemplate with authorization for the provided URL using the given RestTemplateBuilder. 23 | * Authorization credentials will be extracted from the URL (http://username:password@host.example/). 24 | * 25 | * @param restTemplateBuilder the rest template builder to use 26 | * @param url the url to use 27 | * @return rest template 28 | */ 29 | public static RestTemplate buildWithAuth(RestTemplateBuilder restTemplateBuilder, String url) { 30 | UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(url).build(); 31 | String userInfo = uriComponents.getUserInfo(); 32 | if (userInfo != null) { 33 | List auth = Splitter.on(":").splitToList(userInfo); 34 | if (auth.size() == 2) { 35 | return restTemplateBuilder.basicAuthorization(auth.get(0), auth.get(1)).build(); 36 | } else { 37 | LOG.warn("Could not extract username and password: '{}'", userInfo); 38 | } 39 | } 40 | 41 | return restTemplateBuilder.build(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/_1element/sc/utils/URIConstants.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.utils; //NOSONAR 2 | 3 | /** 4 | * Constants for URI building. 5 | */ 6 | public final class URIConstants { 7 | 8 | // API 9 | public static final String API_ROOT = "/api/v1"; 10 | 11 | public static final String API_RECORDINGS = "/recordings"; 12 | 13 | public static final String API_CAMERAS = "/cameras"; 14 | 15 | public static final String API_PUSH_NOTIFICATION_SETTINGS = "/push-notification-settings"; 16 | 17 | public static final String API_PROPERTIES = "/properties"; 18 | 19 | public static final String API_AUTH = "/auth"; 20 | 21 | // Generation 22 | public static final String GENERATE_ROOT = "/generate"; 23 | 24 | public static final String GENERATE_MJPEG = "/mjpeg/{id}"; 25 | 26 | // Proxy 27 | public static final String PROXY_ROOT = "/proxy"; 28 | 29 | public static final String PROXY_SNAPSHOT = "/snapshot/{id}"; 30 | 31 | // Feed 32 | public static final String FEED_ROOT = "/feed"; 33 | 34 | public static final String FEED_CAMERAS = "/cameras"; 35 | 36 | private URIConstants() { 37 | // hide constructor 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | feed.status.title=Surveillance Center Status 2 | feed.status.date=Date 3 | healthcheck.title=Camera health check 4 | healthcheck.message=Camera {0} on host {1} is {2} 5 | push-notification.title=Camera {0} 6 | push-notification.message=Motion detected on camera {0} at timestamp: {1}. 7 | -------------------------------------------------------------------------------- /src/main/resources/messages_de.properties: -------------------------------------------------------------------------------- 1 | feed.status.title=Surveillance Center Status 2 | feed.status.date=Status vom 3 | healthcheck.title=Kamera Erreichbarkeitsprüfung 4 | healthcheck.message=Kamera {0} (Host {1}) ist: {2} 5 | push-notification.title=Kamera {0} 6 | push-notification.message=Die Kamera {0} hat Bewegung erkannt und aufgezeichnet. Zeitstempel: {1}. 7 | -------------------------------------------------------------------------------- /src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Oops an error occurred 9 | 10 | 32 | 33 | 34 | 35 |

404

36 |

Not found

37 |

38 | There seems to be a problem with the page you requested: /error.html
39 | The page was either not found or does not exist. 40 |

41 | 42 | 43 | -------------------------------------------------------------------------------- /src/main/resources/templates/feed-cameras.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Surveillance Center Status 5 | Surveillance Center Status 6 | 7 | Date 8 | 2017-06-25 10:55:13 9 | 10 | 11 | Front door 12 | 124 (2017-06-25 10:45:33) 14 | 0 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/SurveillanceCenterApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.core.io.support.PropertiesLoaderUtils; 7 | import org.springframework.test.context.junit4.SpringRunner; 8 | import org.springframework.test.context.web.WebAppConfiguration; 9 | 10 | import static org.junit.Assert.assertTrue; 11 | 12 | import java.util.Properties; 13 | 14 | @RunWith(SpringRunner.class) 15 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 16 | @WebAppConfiguration 17 | public class SurveillanceCenterApplicationTests { 18 | 19 | @Test 20 | public void testContextLoads() throws Exception { 21 | Properties properties = PropertiesLoaderUtils.loadAllProperties("application.properties"); 22 | assertTrue(properties.size() > 0); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/controller/SurveillanceAuthenticationControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.github._1element.sc.SurveillanceCenterApplication; 5 | import com.github._1element.sc.security.JwtAuthenticationRequest; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | 15 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 17 | 18 | @RunWith(SpringRunner.class) 19 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 20 | @AutoConfigureMockMvc 21 | public class SurveillanceAuthenticationControllerTest { 22 | 23 | @Autowired 24 | private MockMvc mockMvc; 25 | 26 | private static final String AUTH_ENDPOINT = "/api/v1/auth"; 27 | 28 | @Test 29 | public void testCreateAuthenticationToken() throws Exception { 30 | JwtAuthenticationRequest jwtAuthenticationRequest = new JwtAuthenticationRequest("admin", "password"); 31 | 32 | mockMvc.perform(post(AUTH_ENDPOINT) 33 | .contentType(MediaType.APPLICATION_JSON) 34 | .content(new ObjectMapper().writeValueAsString(jwtAuthenticationRequest))) 35 | .andExpect(status().isNoContent()); 36 | } 37 | 38 | @Test 39 | public void testCreateAuthenticationTokenWithInvalidPassword() throws Exception { 40 | JwtAuthenticationRequest jwtAuthenticationRequest = new JwtAuthenticationRequest("admin", "invalid-password"); 41 | 42 | mockMvc.perform(post(AUTH_ENDPOINT) 43 | .contentType(MediaType.APPLICATION_JSON) 44 | .content(new ObjectMapper().writeValueAsString(jwtAuthenticationRequest))) 45 | .andExpect(status().isUnauthorized()); 46 | } 47 | 48 | @Test 49 | public void testCreateAuthenticationTokenWithInvalidUsername() throws Exception { 50 | JwtAuthenticationRequest jwtAuthenticationRequest = new JwtAuthenticationRequest("invalid-username", "password"); 51 | 52 | mockMvc.perform(post(AUTH_ENDPOINT) 53 | .contentType(MediaType.APPLICATION_JSON) 54 | .content(new ObjectMapper().writeValueAsString(jwtAuthenticationRequest))) 55 | .andExpect(status().isUnauthorized()); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/controller/SurveillanceFeedControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.controller; 2 | 3 | import com.github._1element.sc.SurveillanceCenterApplication; 4 | import com.github._1element.sc.domain.Camera; 5 | import com.github._1element.sc.dto.ImagesCameraSummaryResult; 6 | import com.github._1element.sc.service.JwtAuthenticationService; 7 | import com.github._1element.sc.service.SurveillanceService; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | 17 | import javax.servlet.http.Cookie; 18 | import java.time.LocalDateTime; 19 | import java.util.Arrays; 20 | 21 | import static org.hamcrest.CoreMatchers.containsString; 22 | import static org.mockito.BDDMockito.given; 23 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 26 | 27 | @RunWith(SpringRunner.class) 28 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 29 | @AutoConfigureMockMvc 30 | public class SurveillanceFeedControllerTest { 31 | 32 | @Autowired 33 | private MockMvc mockMvc; 34 | 35 | @Autowired 36 | private JwtAuthenticationService jwtAuthenticationService; 37 | 38 | @MockBean 39 | private SurveillanceService surveillanceService; 40 | 41 | @Test 42 | public void testCamerasfeedUnauthorized() throws Exception { 43 | mockMvc.perform(get("/feed/cameras")).andExpect(status().isUnauthorized()); 44 | } 45 | 46 | @Test 47 | public void testCamerasFeed() throws Exception { 48 | // arrange 49 | LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 20, 15, 45, 0, 0); 50 | Camera camera1 = new Camera("idCamera1", "Camera 1", null, null, null, null); 51 | ImagesCameraSummaryResult cameraSummaryResult1 = new ImagesCameraSummaryResult(camera1, 102L, localDateTime); 52 | 53 | Camera camera2 = new Camera("idCamera2", "Camera 2", null, null, null, null); 54 | ImagesCameraSummaryResult cameraSummaryResult2 = new ImagesCameraSummaryResult(camera2, 22L, localDateTime); 55 | 56 | given(surveillanceService.getImagesCameraSummary()) 57 | .willReturn(Arrays.asList(cameraSummaryResult1, cameraSummaryResult2)); 58 | 59 | Cookie tokenCookie = jwtAuthenticationService.generateTokenCookie("admin"); 60 | 61 | // act and assert 62 | mockMvc.perform(get("/feed/cameras").cookie(tokenCookie)) 63 | .andExpect(status().isOk()) 64 | .andExpect(content().string(containsString("Camera 1"))) 65 | .andExpect(content().string(containsString("102 (2018-01-20 15:45:00)"))) 66 | .andExpect(content().string(containsString("Camera 2"))) 67 | .andExpect(content().string(containsString("22 (2018-01-20 15:45:00)"))); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/domain/CameraPictureTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.JUnit4; 6 | 7 | import static org.junit.Assert.assertFalse; 8 | 9 | @RunWith(JUnit4.class) 10 | public class CameraPictureTest { 11 | 12 | @Test(expected = IllegalArgumentException.class) 13 | public void testEmptySnapshotUrl() throws Exception { 14 | new CameraPicture("", true, true); 15 | } 16 | 17 | @Test(expected = IllegalArgumentException.class) 18 | public void testNullSnapshotUrl() throws Exception { 19 | new CameraPicture(null, true, true); 20 | } 21 | 22 | @Test 23 | public void testDisabledSnapshotAndStream() throws Exception { 24 | final CameraPicture cameraPicture = new CameraPicture("", false, false); 25 | 26 | assertFalse(cameraPicture.isSnapshotEnabled()); 27 | assertFalse(cameraPicture.isStreamEnabled()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/domain/pushnotification/PushoverClientTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.pushnotification; 2 | 3 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; 4 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; 5 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest; 6 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; 7 | 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.http.HttpMethod; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.context.web.WebAppConfiguration; 17 | import org.springframework.test.util.ReflectionTestUtils; 18 | import org.springframework.test.web.client.MockRestServiceServer; 19 | import org.springframework.web.client.RestTemplate; 20 | 21 | import com.github._1element.sc.SurveillanceCenterApplication; 22 | import com.github._1element.sc.exception.PushNotificationClientException; 23 | 24 | @RunWith(SpringRunner.class) 25 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 26 | @WebAppConfiguration 27 | public class PushoverClientTest { 28 | 29 | @Autowired 30 | private PushoverClient pushoverClient; 31 | 32 | private MockRestServiceServer mockServer; 33 | 34 | private static final String EXPECTED_ENDPOINT = "https://api.pushover.net/1/messages.json"; 35 | 36 | /** 37 | * Setup for all tests. 38 | * 39 | * @throws Exception exception in case of an error 40 | */ 41 | @Before 42 | public void setUp() throws Exception { 43 | RestTemplate restTemplate = new RestTemplate(); 44 | ReflectionTestUtils.setField(pushoverClient, "restTemplate", restTemplate); 45 | mockServer = MockRestServiceServer.createServer(restTemplate); 46 | } 47 | 48 | @Test 49 | public void testSendMessage() throws Exception { 50 | mockServer.expect(requestTo(EXPECTED_ENDPOINT)) 51 | .andExpect(method(HttpMethod.POST)) 52 | .andRespond(withSuccess("{ \"status\" : 1}", MediaType.APPLICATION_JSON)); 53 | 54 | pushoverClient.sendMessage("Title", "Message text"); 55 | 56 | mockServer.verify(); 57 | } 58 | 59 | @Test(expected = PushNotificationClientException.class) 60 | public void testSendMessageErrorResponse() throws Exception { 61 | mockServer.expect(requestTo(EXPECTED_ENDPOINT)) 62 | .andExpect(method(HttpMethod.POST)) 63 | .andRespond(withBadRequest()); 64 | 65 | pushoverClient.sendMessage("Title", "Message text"); 66 | 67 | mockServer.verify(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/domain/remotecopy/FTPRemoteCopyTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.remotecopy; 2 | 3 | import com.github._1element.sc.SurveillanceCenterApplication; 4 | import com.github._1element.sc.events.RemoteCopyEvent; 5 | import com.github._1element.sc.service.FileService; 6 | 7 | import org.apache.commons.net.ftp.FTPClient; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.Mockito; 14 | import org.mockito.MockitoAnnotations; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.test.context.web.WebAppConfiguration; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | 20 | import java.io.InputStream; 21 | import java.nio.file.Path; 22 | import java.nio.file.Paths; 23 | 24 | import static org.mockito.Matchers.any; 25 | import static org.mockito.Matchers.eq; 26 | import static org.mockito.Mockito.mock; 27 | import static org.mockito.Mockito.verify; 28 | 29 | @RunWith(SpringRunner.class) 30 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 31 | @WebAppConfiguration 32 | public class FTPRemoteCopyTest { 33 | 34 | @Mock 35 | private FTPClient ftpClient; 36 | 37 | @Mock 38 | private FileService fileService; 39 | 40 | @Autowired 41 | @InjectMocks 42 | private FTPRemoteCopy ftpRemoteCopy; 43 | 44 | private static final String EXPECTED_FTP_USERNAME = "ftpuser"; 45 | 46 | private static final String EXPECTED_FTP_PASSWORD = "secret"; 47 | 48 | private static final String EXPECTED_REMOTE_FILENAME = "/remote-copy-directory/local-file.jpg"; 49 | 50 | private static final String EXPECTED_LOCAL_FILE_PATH = "/tmp/test/local-file.jpg"; 51 | 52 | /** 53 | * Setup for all tests. 54 | * 55 | * @throws Exception exception in case of an error. 56 | */ 57 | @Before 58 | public void setUp() throws Exception { 59 | MockitoAnnotations.initMocks(this); 60 | Mockito.when(ftpClient.getReplyCode()).thenReturn(200); 61 | Mockito.when(ftpClient.login(eq(EXPECTED_FTP_USERNAME), eq(EXPECTED_FTP_PASSWORD))).thenReturn(true); 62 | } 63 | 64 | @Test 65 | public void testHandle() throws Exception { 66 | // mocking 67 | Mockito.when(ftpClient.storeFile(eq(EXPECTED_REMOTE_FILENAME), any())).thenReturn(true); 68 | 69 | Path pathMock = mock(Path.class); 70 | Mockito.when(pathMock.getFileName()).thenReturn(Paths.get("local-file.jpg")); 71 | Mockito.when(fileService.getPath(EXPECTED_LOCAL_FILE_PATH)).thenReturn(pathMock); 72 | 73 | InputStream inputStreamMock = mock(InputStream.class); 74 | Mockito.when(fileService.createInputStream(any(Path.class))).thenReturn(inputStreamMock); 75 | 76 | // execute and verify 77 | RemoteCopyEvent remoteCopyEvent = new RemoteCopyEvent(EXPECTED_LOCAL_FILE_PATH); 78 | ftpRemoteCopy.handle(remoteCopyEvent); 79 | 80 | verify(ftpClient).login(eq(EXPECTED_FTP_USERNAME), eq(EXPECTED_FTP_PASSWORD)); 81 | verify(ftpClient).storeFile(eq(EXPECTED_REMOTE_FILENAME), any()); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/domain/remotecopy/SFTPRemoteCopyTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.domain.remotecopy; 2 | 3 | import static org.mockito.Matchers.any; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.verify; 6 | 7 | import java.io.InputStream; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.InjectMocks; 15 | import org.mockito.Mock; 16 | import org.mockito.Mockito; 17 | import org.mockito.MockitoAnnotations; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.boot.test.context.SpringBootTest; 20 | import org.springframework.test.context.junit4.SpringRunner; 21 | import org.springframework.test.context.web.WebAppConfiguration; 22 | 23 | import com.github._1element.sc.SurveillanceCenterApplication; 24 | import com.github._1element.sc.events.RemoteCopyEvent; 25 | import com.github._1element.sc.properties.SFTPRemoteCopyProperties; 26 | import com.github._1element.sc.service.FileService; 27 | import com.jcraft.jsch.ChannelSftp; 28 | import com.jcraft.jsch.JSch; 29 | import com.jcraft.jsch.Session; 30 | 31 | @RunWith(SpringRunner.class) 32 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 33 | @WebAppConfiguration 34 | public class SFTPRemoteCopyTest { 35 | 36 | @Autowired 37 | private SFTPRemoteCopyProperties sftpRemoteCopyProperties; 38 | 39 | @Mock 40 | private JSch jsch; 41 | 42 | @Mock 43 | private FileService fileService; 44 | 45 | @Autowired 46 | @InjectMocks 47 | private SFTPRemoteCopy sftpRemoteCopy; 48 | 49 | private static final String EXPECTED_LOCAL_FILE_PATH = "/tmp/test/local-file.jpg"; 50 | 51 | private static final String EXPECTED_FILE_NAME = "local-file.jpg"; 52 | 53 | @Before 54 | public void setUp() throws Exception { 55 | MockitoAnnotations.initMocks(this); 56 | } 57 | 58 | @Test 59 | public void testHandle() throws Exception { 60 | // mocking 61 | Session sessionMock = mock(Session.class); 62 | ChannelSftp channelMock = mock(ChannelSftp.class); 63 | 64 | Mockito.when(sessionMock.openChannel(any())).thenReturn(channelMock); 65 | Mockito.when(jsch.getSession(sftpRemoteCopyProperties.getUsername(), sftpRemoteCopyProperties.getHost())) 66 | .thenReturn(sessionMock); 67 | 68 | Path pathMock = mock(Path.class); 69 | Mockito.when(pathMock.getFileName()).thenReturn(Paths.get(EXPECTED_FILE_NAME)); 70 | Mockito.when(fileService.getPath(EXPECTED_LOCAL_FILE_PATH)).thenReturn(pathMock); 71 | 72 | InputStream inputStreamMock = mock(InputStream.class); 73 | Mockito.when(fileService.createInputStream(any(Path.class))).thenReturn(inputStreamMock); 74 | 75 | // execute 76 | RemoteCopyEvent remoteCopyEvent = new RemoteCopyEvent(EXPECTED_LOCAL_FILE_PATH); 77 | sftpRemoteCopy.handle(remoteCopyEvent); 78 | 79 | // verify 80 | verify(channelMock).cd(sftpRemoteCopyProperties.getDir()); 81 | verify(channelMock).put(inputStreamMock, EXPECTED_FILE_NAME); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/properties/MultiCameraAwarePropertiesTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.properties; 2 | 3 | import com.github._1element.sc.SurveillanceCenterApplication; 4 | import com.github._1element.sc.exception.PropertyNotFoundException; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | import org.springframework.test.context.web.WebAppConfiguration; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | import static org.junit.Assert.assertFalse; 14 | import static org.junit.Assert.assertTrue; 15 | 16 | @RunWith(SpringRunner.class) 17 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 18 | @WebAppConfiguration 19 | public class MultiCameraAwarePropertiesTest { 20 | 21 | @Autowired 22 | private MultiCameraAwareProperties multiCameraAwareProperties; 23 | 24 | @Test 25 | public void testGetProperty() throws Exception { 26 | assertEquals("Front door", multiCameraAwareProperties.getProperty( 27 | MultiCameraAwareProperties.PROPERTY_MULTI_CAMERA_PREFIX + "name", "testcamera1")); 28 | } 29 | 30 | @Test(expected = PropertyNotFoundException.class) 31 | public void testGetPropertyNotFound() throws Exception { 32 | multiCameraAwareProperties.getProperty( 33 | MultiCameraAwareProperties.PROPERTY_MULTI_CAMERA_PREFIX + "invalidKey", "invalidCameraId"); 34 | } 35 | 36 | @Test 37 | public void testGetPropertyBoolean() throws Exception { 38 | assertFalse(multiCameraAwareProperties.getProperty(MultiCameraAwareProperties.PROPERTY_MULTI_CAMERA_PREFIX 39 | + "stream-enabled", "testcamera2", boolean.class)); 40 | 41 | assertTrue(multiCameraAwareProperties.getProperty(MultiCameraAwareProperties.PROPERTY_MULTI_CAMERA_PREFIX 42 | + "snapshot-enabled", "testcamera2", boolean.class)); 43 | } 44 | 45 | @Test 46 | public void testGetPropertyWithDefault() throws Exception { 47 | assertEquals("Backyard", multiCameraAwareProperties.getProperty( 48 | MultiCameraAwareProperties.PROPERTY_MULTI_CAMERA_PREFIX + "name", "testcamera2", String.class, 49 | "DefaultWillNotBeUsed")); 50 | 51 | assertEquals("PassedDefault", multiCameraAwareProperties.getProperty( 52 | MultiCameraAwareProperties.PROPERTY_MULTI_CAMERA_PREFIX + "invalidKey", "testcamera2", String.class, 53 | "PassedDefault")); 54 | 55 | assertEquals("PassedDefaultCameraValue", multiCameraAwareProperties.getProperty( 56 | MultiCameraAwareProperties.PROPERTY_MULTI_CAMERA_PREFIX + "name", "invalidCamera", String.class, 57 | "PassedDefaultCameraValue")); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/security/JwtAuthenticationEntryPointTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.security; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.JUnit4; 6 | import org.springframework.mock.web.MockHttpServletRequest; 7 | import org.springframework.mock.web.MockHttpServletResponse; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | @RunWith(JUnit4.class) 12 | public class JwtAuthenticationEntryPointTest { 13 | 14 | @Test 15 | public void testCommence() throws Exception { 16 | JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint = new JwtAuthenticationEntryPoint(); 17 | MockHttpServletResponse httpServletResponse = new MockHttpServletResponse(); 18 | 19 | jwtAuthenticationEntryPoint.commence(new MockHttpServletRequest(), httpServletResponse,null); 20 | 21 | assertEquals(401, httpServletResponse.getStatus()); 22 | assertEquals("Unauthorized", httpServletResponse.getErrorMessage()); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/service/CleanupTasksTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; 2 | 3 | import com.github._1element.sc.domain.SurveillanceImage; 4 | import com.github._1element.sc.properties.ImageProperties; 5 | import com.github._1element.sc.repository.SurveillanceImageRepository; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.junit.runners.JUnit4; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.MockitoAnnotations; 13 | import org.springframework.test.util.ReflectionTestUtils; 14 | 15 | import java.nio.file.Path; 16 | import java.time.LocalDateTime; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | 20 | import static org.mockito.Matchers.any; 21 | import static org.mockito.Mockito.times; 22 | import static org.mockito.Mockito.verify; 23 | import static org.mockito.Mockito.verifyZeroInteractions; 24 | import static org.mockito.Mockito.when; 25 | 26 | @RunWith(JUnit4.class) 27 | public class CleanupTasksTest { 28 | 29 | @Mock 30 | private SurveillanceImageRepository imageRepository; 31 | 32 | @Mock 33 | private ImageProperties imageProperties; 34 | 35 | @Mock 36 | private FileService fileService; 37 | 38 | @InjectMocks 39 | private CleanupTasks cleanupTasks; 40 | 41 | /** 42 | * Setup for all tests. 43 | * 44 | * @throws Exception exception in case of an error 45 | */ 46 | @Before 47 | public void setUp() throws Exception { 48 | MockitoAnnotations.initMocks(this); 49 | 50 | // test data 51 | final SurveillanceImage image1 = new SurveillanceImage("file1.jpg", "camera1", LocalDateTime.now()); 52 | final SurveillanceImage image2 = new SurveillanceImage("file2.jpg", "camera2", LocalDateTime.now()); 53 | final List images = Arrays.asList(image1, image2); 54 | 55 | // mocking 56 | when(imageRepository.getArchivedImagesToCleanup(any())).thenReturn(images); 57 | when(imageProperties.getStorageDir()).thenReturn("/tmp/storage-dir/"); 58 | 59 | cleanupTasks = new CleanupTasks(imageRepository, fileService, imageProperties); 60 | 61 | ReflectionTestUtils.setField(cleanupTasks, "keepHours", 24); 62 | } 63 | 64 | @Test 65 | public void testCleanupArchive() throws Exception { 66 | ReflectionTestUtils.setField(cleanupTasks, "isCleanupEnabled", true); 67 | 68 | cleanupTasks.cleanupArchive(); 69 | 70 | // verify method calls to delete files (2 images + 2 thumbnails) 71 | verify(fileService, times(4)).delete(any(Path.class)); 72 | 73 | // verify image repository db deletion for 2 images 74 | verify(imageRepository, times(2)).delete(any(SurveillanceImage.class)); 75 | } 76 | 77 | @Test 78 | public void testCleanupArchiveDisabled() throws Exception { 79 | ReflectionTestUtils.setField(cleanupTasks, "isCleanupEnabled", false); 80 | 81 | cleanupTasks.cleanupArchive(); 82 | 83 | verifyZeroInteractions(imageRepository); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/service/FileServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.JUnit4; 6 | 7 | import com.github._1element.sc.properties.ImageProperties; 8 | 9 | import static org.junit.Assert.assertFalse; 10 | import static org.junit.Assert.assertNotNull; 11 | import static org.junit.Assert.assertTrue; 12 | import static org.junit.Assert.assertNotEquals; 13 | 14 | @RunWith(JUnit4.class) 15 | public class FileServiceTest { 16 | 17 | @Test 18 | public void testHasValidExtensionWithTwoExtensions() throws Exception { 19 | // two extensions 20 | ImageProperties imageProperties = new ImageProperties(); 21 | imageProperties.setValidExtensions(new String[]{".jpg", ".jpeg"}); 22 | 23 | FileService fileService = new FileService(imageProperties); 24 | 25 | assertTrue(fileService.hasValidExtension("test-file.jpg")); 26 | assertTrue(fileService.hasValidExtension("another-test-file.jpeg")); 27 | assertFalse(fileService.hasValidExtension("file-without-extension")); 28 | assertFalse(fileService.hasValidExtension("file-with-wrong-extenions.png")); 29 | assertFalse(fileService.hasValidExtension(null)); 30 | } 31 | 32 | @Test 33 | public void testHasValidExtensionWithOneExtension() throws Exception { 34 | // just one extension configured 35 | ImageProperties imageProperties = new ImageProperties(); 36 | imageProperties.setValidExtensions(new String[]{".jpg"}); 37 | 38 | FileService fileService = new FileService(imageProperties); 39 | 40 | assertTrue(fileService.hasValidExtension("okay.jpg")); 41 | assertFalse(fileService.hasValidExtension("not-okay.jpeg")); 42 | } 43 | 44 | @Test 45 | public void testHasValidExtensionWithNoConfiguration() throws Exception { 46 | // no extension configured 47 | ImageProperties imageProperties = new ImageProperties(); 48 | imageProperties.setValidExtensions(null); 49 | 50 | FileService fileService = new FileService(imageProperties); 51 | 52 | assertTrue(fileService.hasValidExtension("something.jpg")); 53 | assertTrue(fileService.hasValidExtension("something-completely-different.txt")); 54 | } 55 | 56 | @Test 57 | public void testGetUniquePrefix() throws Exception { 58 | FileService fileService = new FileService(null); 59 | 60 | String result1 = fileService.getUniquePrefix(); 61 | String result2 = fileService.getUniquePrefix(); 62 | 63 | assertNotNull(result1); 64 | assertNotNull(result2); 65 | assertTrue(result1.matches("\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{3}-(.){7}")); 66 | assertNotEquals(result1, result2); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/service/JwtUserDetailsServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; 2 | 3 | import com.github._1element.sc.SurveillanceCenterApplication; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | import org.springframework.test.context.web.WebAppConfiguration; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | @RunWith(SpringRunner.class) 16 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 17 | @WebAppConfiguration 18 | public class JwtUserDetailsServiceTest { 19 | 20 | @Autowired 21 | private JwtUserDetailsService jwtUserDetailsService; 22 | 23 | @Test 24 | public void testLoadUserByUsername() throws Exception { 25 | UserDetails userDetailsResult = jwtUserDetailsService.loadUserByUsername("admin"); 26 | 27 | assertEquals("admin", userDetailsResult.getUsername()); 28 | assertEquals("$2a$04$xdRJiiGwwHEbSgs6ucM0DOOCVEUQVaKtB3UPO16.h65sCWzPlkFHC", userDetailsResult.getPassword()); 29 | assertEquals("[ROLE_USER]", userDetailsResult.getAuthorities().toString()); 30 | } 31 | 32 | @Test(expected = UsernameNotFoundException.class) 33 | public void testLoadUserByUsernameNotFound() throws Exception { 34 | jwtUserDetailsService.loadUserByUsername("invalid-username"); 35 | } 36 | 37 | @Test(expected = UsernameNotFoundException.class) 38 | public void testLoadUserByUsernameEmpty() throws Exception { 39 | jwtUserDetailsService.loadUserByUsername(""); 40 | } 41 | 42 | @Test(expected = NullPointerException.class) 43 | public void testLoadUserByUsernameNull() throws Exception { 44 | jwtUserDetailsService.loadUserByUsername(null); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/service/ModelMappingServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; 2 | 3 | import com.github._1element.sc.SurveillanceCenterApplication; 4 | import com.github._1element.sc.domain.Camera; 5 | import com.github._1element.sc.domain.CameraPicture; 6 | import com.github._1element.sc.domain.SurveillanceImage; 7 | import com.github._1element.sc.dto.CameraResource; 8 | import com.github._1element.sc.dto.SurveillanceImageResource; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | import org.springframework.test.context.web.WebAppConfiguration; 15 | 16 | import java.time.LocalDateTime; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertFalse; 20 | 21 | @RunWith(SpringRunner.class) 22 | @SpringBootTest(classes = SurveillanceCenterApplication.class) 23 | @WebAppConfiguration 24 | public class ModelMappingServiceTest { 25 | 26 | @Autowired 27 | private ModelMappingService modelMappingService; 28 | 29 | @Test 30 | public void testConvertCameraToResource() throws Exception { 31 | // arrange 32 | final CameraPicture picture = new CameraPicture("http://internal.example/snapshot.cgi", true, true); 33 | final Camera camera = new Camera("front", "Camera Name", "internal-host.example", "mqttTopic", null, picture); 34 | 35 | // act 36 | final CameraResource cameraResource = modelMappingService.convertCameraToResource(camera); 37 | 38 | // assert 39 | assertEquals("front", cameraResource.getId()); 40 | assertEquals("Camera Name", cameraResource.getName()); 41 | assertEquals("http://localhost/proxy/snapshot/front", cameraResource.getSnapshotProxyUrl()); 42 | assertEquals("http://localhost/generate/mjpeg/front", cameraResource.getStreamGeneratorUrl()); 43 | } 44 | 45 | @Test 46 | public void testConvertSurveillanceImageToResource() throws Exception { 47 | // arrange 48 | final LocalDateTime localDateTime = LocalDateTime.now(); 49 | final SurveillanceImage surveillanceImage = new SurveillanceImage("fileName.jpg", "testcamera1", localDateTime); 50 | 51 | // act 52 | final SurveillanceImageResource surveillanceImageResource = 53 | modelMappingService.convertSurveillanceImageToResource(surveillanceImage); 54 | 55 | // assert 56 | assertEquals("testcamera1", surveillanceImageResource.getCameraId()); 57 | assertEquals("Front door", surveillanceImageResource.getCameraName()); 58 | assertEquals("fileName.jpg", surveillanceImageResource.getFileName()); 59 | assertEquals(localDateTime, surveillanceImageResource.getReceivedAt()); 60 | assertFalse(surveillanceImage.isArchived()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/service/MqttServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.service; 2 | 3 | import com.github._1element.sc.domain.Camera; 4 | import com.github._1element.sc.events.ImageReceivedEvent; 5 | import com.github._1element.sc.properties.MqttProperties; 6 | import com.github._1element.sc.repository.CameraRepository; 7 | import org.eclipse.paho.client.mqttv3.MqttClient; 8 | import org.eclipse.paho.client.mqttv3.MqttMessage; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.junit.runners.JUnit4; 13 | import org.mockito.InjectMocks; 14 | import org.mockito.Mock; 15 | import org.mockito.MockitoAnnotations; 16 | import org.springframework.context.ApplicationEventPublisher; 17 | import org.springframework.test.util.ReflectionTestUtils; 18 | 19 | import static org.mockito.Matchers.refEq; 20 | import static org.mockito.Mockito.mock; 21 | import static org.mockito.Mockito.verify; 22 | import static org.mockito.Mockito.verifyZeroInteractions; 23 | import static org.mockito.Mockito.when; 24 | 25 | @RunWith(JUnit4.class) 26 | public class MqttServiceTest { 27 | 28 | @Mock 29 | private CameraRepository cameraRepository; 30 | 31 | @Mock 32 | private ApplicationEventPublisher eventPublisher; 33 | 34 | @Mock 35 | private MqttProperties properties; 36 | 37 | @Mock 38 | private MqttClient mqttClient; 39 | 40 | @InjectMocks 41 | private MqttService mqttService; 42 | 43 | @Before 44 | public void setUp() throws Exception { 45 | MockitoAnnotations.initMocks(this); 46 | mqttService = new MqttService(cameraRepository, eventPublisher, properties); 47 | } 48 | 49 | @Test 50 | public void testMessageArrived() throws Exception { 51 | // arrange 52 | byte[] payload = "payload".getBytes(); 53 | MqttMessage mqttMessage = mock(MqttMessage.class); 54 | when(mqttMessage.getPayload()).thenReturn(payload); 55 | Camera camera = mock(Camera.class); 56 | when(cameraRepository.findByMqttTopic("mqttTopic")).thenReturn(camera); 57 | ImageReceivedEvent expectedImageReceivedEvent = new ImageReceivedEvent(payload, camera); 58 | 59 | // act 60 | mqttService.messageArrived("mqttTopic", mqttMessage); 61 | 62 | // assert 63 | verify(eventPublisher).publishEvent(refEq(expectedImageReceivedEvent)); 64 | } 65 | 66 | @Test 67 | public void testMessageArrivedNoTopic() throws Exception { 68 | // arrange 69 | MqttMessage mqttMessage = mock(MqttMessage.class); 70 | when(cameraRepository.findByMqttTopic("unknownMqttTopic")).thenReturn(null); 71 | 72 | // act 73 | mqttService.messageArrived("unknownMqttTopic", mqttMessage); 74 | 75 | // assert 76 | verifyZeroInteractions(eventPublisher); 77 | } 78 | 79 | @Test 80 | public void testConnectComplete() throws Exception { 81 | // arrange 82 | ReflectionTestUtils.setField(mqttService, "mqttClient", mqttClient); 83 | 84 | // act 85 | mqttService.connectComplete(false, "brokerUri"); 86 | 87 | // assert 88 | verifyZeroInteractions(mqttClient); 89 | } 90 | 91 | @Test 92 | public void testConnectCompleteReconnect() throws Exception { 93 | // arrange 94 | when(properties.getTopicFilter()).thenReturn("topic-filter"); 95 | ReflectionTestUtils.setField(mqttService, "mqttClient", mqttClient); 96 | 97 | // act 98 | mqttService.connectComplete(true, "brokerUri"); 99 | 100 | // assert 101 | verify(mqttClient).subscribe("topic-filter"); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/github/_1element/sc/utils/RestTemplateUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github._1element.sc.utils; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.JUnit4; 6 | import org.springframework.boot.web.client.RestTemplateBuilder; 7 | import org.springframework.http.client.support.BasicAuthorizationInterceptor; 8 | import org.springframework.test.util.ReflectionTestUtils; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | import static org.junit.Assert.assertTrue; 13 | 14 | @RunWith(JUnit4.class) 15 | public class RestTemplateUtilsTest { 16 | 17 | @Test 18 | public void testBuildWithAuthNoCredentials() throws Exception { 19 | RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); 20 | 21 | RestTemplate restTemplateResult = RestTemplateUtils.buildWithAuth(restTemplateBuilder, "http://host.example/path"); 22 | 23 | assertEquals(0, restTemplateResult.getInterceptors().size()); 24 | } 25 | 26 | @Test 27 | public void testBuildWithAuthCredentials() throws Exception { 28 | RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); 29 | 30 | RestTemplate restTemplateResult = RestTemplateUtils.buildWithAuth(restTemplateBuilder, "http://user:pass@host.example/command.cgi"); 31 | 32 | assertTrue(restTemplateResult.getInterceptors().get(0) instanceof BasicAuthorizationInterceptor); 33 | assertEquals("user", ReflectionTestUtils.getField(restTemplateResult.getInterceptors().get(0), "username")); 34 | assertEquals("pass", ReflectionTestUtils.getField(restTemplateResult.getInterceptors().get(0), "password")); 35 | } 36 | 37 | @Test 38 | public void testBuildWithAuthEmptyPassword() throws Exception { 39 | RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); 40 | 41 | RestTemplate restTemplateResult = RestTemplateUtils.buildWithAuth(restTemplateBuilder, "http://username:@host.example/command.cgi"); 42 | 43 | assertTrue(restTemplateResult.getInterceptors().get(0) instanceof BasicAuthorizationInterceptor); 44 | assertEquals("username", ReflectionTestUtils.getField(restTemplateResult.getInterceptors().get(0), "username")); 45 | assertEquals("", ReflectionTestUtils.getField(restTemplateResult.getInterceptors().get(0), "password")); 46 | } 47 | 48 | @Test 49 | public void testBuildWithAuthMissingPassword() throws Exception { 50 | RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); 51 | 52 | RestTemplate restTemplateResult = RestTemplateUtils.buildWithAuth(restTemplateBuilder, "http://something@host.example/command.cgi"); 53 | 54 | assertEquals(0, restTemplateResult.getInterceptors().size()); 55 | } 56 | 57 | } 58 | --------------------------------------------------------------------------------