├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── demo.png ├── diagram.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── SecureDiary ├── css │ ├── alertify.min.css │ ├── alertify.rtl.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── bootstrap.rtl.min.css │ ├── default.css │ ├── default.min.css │ ├── default.rtl.min.css │ ├── semantic.css │ ├── semantic.min.css │ ├── semantic.rtl.min.css │ └── styles.css ├── img │ ├── i_add.png │ ├── i_delete.png │ ├── i_edit.png │ ├── i_offline.png │ ├── i_online.png │ ├── refresh.png │ ├── register.png │ └── remove.png ├── index.html ├── js │ ├── alertify.min.js │ ├── html5.js │ ├── jquery-1.7.2.min.js │ ├── jquery.touchSwipe.js │ ├── require.js │ └── script.js └── screenshots │ ├── screenshot1.png │ ├── screenshot2.png │ ├── screenshot3.png │ └── screenshot4.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── main ├── java │ └── com │ │ └── github │ │ └── maxamel │ │ └── server │ │ ├── EnumUtils.java │ │ ├── IdentifierType.java │ │ ├── ZKAuthApplication.java │ │ ├── config │ │ ├── KafkaConfig.java │ │ └── LoggerConfig.java │ │ ├── domain │ │ ├── model │ │ │ ├── AuditableEntity.java │ │ │ ├── Constants.java │ │ │ ├── Diary.java │ │ │ ├── User.java │ │ │ ├── constraints │ │ │ │ ├── DataUniqueConstraint.java │ │ │ │ ├── UserEntryNameUnique.java │ │ │ │ └── UserNameUnique.java │ │ │ └── types │ │ │ │ └── SessionStatus.java │ │ └── repositories │ │ │ ├── DiaryRepository.java │ │ │ └── UserRepository.java │ │ ├── services │ │ ├── DiaryService.java │ │ ├── KafkaAgentService.java │ │ ├── ScheduleTaskService.java │ │ ├── UserService.java │ │ ├── impl │ │ │ ├── DiaryServiceImpl.java │ │ │ ├── KafkaAgentServiceImpl.java │ │ │ ├── ScheduleTaskServiceImpl.java │ │ │ └── UserServiceImpl.java │ │ └── mapping │ │ │ └── MapperConfiguration.java │ │ └── web │ │ ├── controllers │ │ ├── DiaryController.java │ │ └── UserController.java │ │ ├── dtos │ │ ├── ChallengeDto.java │ │ ├── DiaryDto.java │ │ ├── ErrorCodes.java │ │ ├── UserDto.java │ │ ├── audit │ │ │ └── AuditableDto.java │ │ └── errors │ │ │ ├── ErrorDto.java │ │ │ ├── HttpMediaTypeErrorDto.java │ │ │ ├── HttpRequestMethodErrorDto.java │ │ │ └── ValidationErrorDto.java │ │ └── handlers │ │ ├── DataExceptionHandlers.java │ │ ├── GlobalErrorHandlers.java │ │ └── ValidationErrorHandlers.java └── resources │ ├── META-INF │ └── spring-devtools.properties │ ├── application.yml │ ├── banner.txt │ ├── bootstrap.yml │ ├── db │ └── migration │ │ └── V1__init.sql │ └── plantuml └── test └── java └── com └── github └── maxamel └── server ├── config └── JsonConfiguration.java ├── domain ├── DiaryRepositoryTest.java └── UserRepositoryTest.java ├── services ├── DiaryServiceTest.java ├── KafkaAgentServiceTest.java ├── ScheduleTaskServiceTest.java ├── UserServiceTest.java └── mapping │ ├── MappingBasePackage.java │ ├── MappingValidateTest.java │ └── UserMappingTest.java └── web ├── DiaryControllerTest.java └── UserControllerTest.java /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | install: true 4 | 5 | addons: 6 | sonarcloud: 7 | organization: "maxamel-github" 8 | token: $SONAR_TOKEN 9 | 10 | jdk: 11 | - oraclejdk8 12 | - openjdk8 13 | 14 | script: 15 | - ./gradlew sonarqube 16 | 17 | cache: 18 | directories: 19 | - '$HOME/.m2/repository' 20 | - '$HOME/.sonar/cache' 21 | - '$HOME/.gradle' 22 | - '.gradle' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Max Amelchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis CI](https://travis-ci.org/maxamel/SpringZKAuth.svg)](https://travis-ci.org/maxamel/SpringZKAuth)
2 | [![Code Coverage](https://sonarcloud.io/api/project_badges/measure?project=com.github.maxamel%3ASpringZKAuth&metric=coverage)](https://sonarcloud.io/api/project_badges/measure?project=com.github.maxamel%3ASpringZKAuth&metric=coverage)
3 | [![Known Vulnerabilities](https://snyk.io/test/github/maxamel/SpringZKAuth/badge.svg)](https://snyk.io/test/github/maxamel/SpringZKAuth) 4 | 5 | [![Quality Gate](https://sonarcloud.io/api/project_badges/quality_gate?project=com.github.maxamel:SpringZKAuth)](https://sonarcloud.io/api/project_badges/quality_gate?project=com.github.maxamel:SpringZKAuth)
6 | 7 | # SpringZKAuth : A zero-knowledge authentication application 8 | 9 | A Spring project utilizing zero-knowledge password proof for secure and private authentication. Users are continuously authenticated throughout their session using rotating session IDs. 10 | 11 | # Overview 12 | 13 | [Zero-knowledge](https://en.wikipedia.org/wiki/Zero-knowledge_proof) is a cryptographic method one can use when required to prove knowledge of a certain secret without revealing anything about the secret itself. 14 | This method is utilized in this project to provide a zero-knowledge password proof (ZKPP) authentication mechanism, meaning proving the knowledge of a password without revealing anything about it. Traditionally, when a user logs into a system, he transmits his user and password over the (possibly encrypted) network. Various security methods exist in order to ensure this password is stored securely, such as hashing, salting, etc. 15 | This project eliminates the need for any of these methods. Specifically, the password of the user is kept completely secret, and is never transmitted over the wire to the server. The server receives only a cryptographic representation (i.e., a one-way function) of the password, that is irreversible and no one can deduce the original password from it. Additionally, the session ID of the user is periodically rotated on the server side (optional). This session ID is also kept secret and the server only transmits a hint via a message broker (e.g., Kafka) about the new session ID. These hints rely on the [discrete logarithm](https://en.wikipedia.org/wiki/Discrete_logarithm) problem, to ensure only the user can compute the necessary information, and no one else. This makes session hijacking practically useless since the session ID is only valid for a very short period of time (configurable). 16 | 17 | # Usage 18 | 19 | The purpose of the project is to provide a POC-level system, and a demonstration of how the concept of zero-knowledge can assist in application security. 20 | If you want to build a RESTful service which provides enhanced security and privacy through ZKPP and continuous authentication, then you can use this project as a starting point. 21 | However, the content to be served by the service is up to you. Currently the guts of the application is just keeping diary entries of users and providing secure, authenticated access to them. You can add your own APIs, DB tables, and all the rest, according to the needs of your own application. 22 | 23 | 24 | # How does it work? 25 | 26 | Let's dive into the nuts and bolts of the cryptographic magic going on behind the scenes. 27 | Firstly, there are two large numbers that are publicly known to everyone. These are the cyclic group generator (g) and a large prime (N). These numbers are carefully picked out, so do not touch them unless you know what you're doing. 28 | When a user wants to register to the system he provides a password as an input (on client-side only) and this password is hashed to produce a unique representation of the password, call it x. The client program then computes (g^x mod N) and sends it to the server-side. Note that neither the server, nor anyone else listening in on the communication can derive x from (g^x mod N). The server saves that information. When the user wants to start consuming APIs, he inputs the password for the user, and waits for a challenge from the server. The server comes up with a large number y, and challenges the user with (g^y mod N). Now both client and server can compute the solution to the challenge. The client by performing ((g^y)^x mod N) and the server by performing ((g^x)^y mod N). Once the server receives the solution from the client he can verify it against his own and grant access if it is correct. From this point on, this solution serves as the session ID for that user. Optionally, you can configure the server to issue a new challenge via a Kafka message broker. These challenges are issued in configurable intervals, and are effective immediately, so all future client requests must contain the new session ID. 29 | 30 | Here is an example of a client registering and then making arbitrary requests. 31 | 32 |

33 | 34 |

35 | 36 | Note that session ID rotating is not described in the diagram, but it is explained further on. 37 | 38 | 39 | # Features 40 | 41 | * Register and remove users 42 | * Write/Edit/Remove diaries per user 43 | * Authentication using zero-knowledge password proof 44 | * Continuous authentication by publishing challenges to Kafka message broker (Optional) 45 | * Configurable session inactivity thresholds 46 | * A client-side GUI application to interact with the system 47 | * No login API, after first password prompt all future authentications are done in the background 48 | * High test coverage 49 | * Automatic scale-up and scale-down of Kafka topics according to active users 50 | 51 | # Prerequisites 52 | 53 | * Java 8 or above 54 | 55 | * NodeJS 5.6.0 or above 56 | 57 | * Confluent Open Source Platform 2.11 (Optional) 58 | 59 | * H2 database 60 | 61 | * Gradle 4.3 or above 62 | 63 | 64 | # Installation 65 | 66 | Assuming you have the above programs installed, follow the below steps to install: 67 | 68 | Clone and build the repository. 69 | ``` 70 | git clone https://github.com/maxamel/SpringZKAuth.git 71 | cd SpringZKAuth 72 | gradle clean build 73 | ``` 74 | 75 | From the command line install the following modules: 76 | ```javascript 77 | npm install big-integer 78 | 79 | npm install jquery 80 | npm install -g browserify 81 | npm install alertify 82 | ``` 83 | 84 | Go to src/SecureDiary/js and run: 85 | ``` 86 | browserify script.js -o bundle.js 87 | ``` 88 | 89 | If you don't want to use Kafka (for continuous authentication) you need to disable it from src/main/resources/application.yml: 90 | ``` 91 | kafka: 92 | enabled: false 93 | ``` 94 | 95 | If you want the continuous authentication feature follow the below steps to install [Confluent Platform](https://docs.confluent.io/current/installation/) which comes bundled with Kafka, Zookeeper and a bunch of other useful software. Here are instructions for Ubuntu or CentOS installations: 96 | 97 | ## Ubuntu 98 | Add repository and key: 99 | ``` 100 | wget -qO - https://packages.confluent.io/deb/4.0/archive.key | sudo apt-key add - 101 | sudo add-apt-repository "deb [arch=amd64] https://packages.confluent.io/deb/4.0 stable main" 102 | ``` 103 | Install and run: 104 | ``` 105 | sudo apt-get update && sudo apt-get install confluent-platform-oss-2.11 106 | confluent start 107 | ``` 108 | 109 | ## CentOS 110 | Add Package: 111 | ``` 112 | sudo rpm --import https://packages.confluent.io/rpm/4.0/archive.key 113 | ``` 114 | 115 | Add file named confluent.repo to /etc/yum.repos.d/ with the contents: 116 | ``` 117 | [Confluent.dist] 118 | name=Confluent repository (dist) 119 | baseurl=https://packages.confluent.io/rpm/4.0/7 120 | gpgcheck=1 121 | gpgkey=https://packages.confluent.io/rpm/4.0/archive.key 122 | enabled=1 123 | 124 | [Confluent] 125 | name=Confluent repository 126 | baseurl=https://packages.confluent.io/rpm/4.0 127 | gpgcheck=1 128 | gpgkey=https://packages.confluent.io/rpm/4.0/archive.key 129 | enabled=1 130 | ``` 131 | 132 | Clean up, install and run: 133 | ``` 134 | sudo yum clean all 135 | sudo yum install confluent-platform-oss-2.11 136 | confluent start 137 | ``` 138 | 139 | # Configuration 140 | 141 | Open your Kafka server.properties file (usually it's in /etc/kafka/) and make sure the following lines are present: 142 | ``` 143 | auto.create.topics.enable=true 144 | num.partitions=1 145 | listeners=PLAINTEXT://YOUR_KAFKA_IP:9092 146 | zookeeper.connect=YOUR_ZOOKEEPER_IP:2181 147 | delete.topic.enable=true 148 | log.retention.ms=60000 149 | ``` 150 | Open your kafka-rest.properties file (usually it's in /etc/kafka-rest/) and make sure the following lines are present: 151 | ``` 152 | zookeeper.connect=YOUR_ZOOKEEPER_IP:2181 153 | bootstrap.servers=PLAINTEXT://YOUR_KAFKA_IP:9092 154 | ``` 155 | 156 | Enable Kafka in src/main/resources/application.yml and fill in the kafka and zookeeper IP: 157 | ``` 158 | kafka: 159 | enabled: true 160 | zookeeper: 161 | url: "YOUR_ZOOKEEPER_IP:2181" 162 | broker: 163 | url: "YOUR_KAFKA_IP:9092" 164 | ``` 165 | Restart Confluent: 166 | ``` 167 | confluent stop 168 | confluent start 169 | ``` 170 | 171 | Lastly, you can adjust the inactivity threshold and challenge intervals (in milliseconds) in src/main/resources/application.yml. 172 | The challenge Frequency is only valid if Kafka is enabled. 173 | ``` 174 | security: 175 | basic: 176 | enabled: false 177 | session: 178 | inactivityKickOut: 120000 179 | challengeFrequency: 30000 180 | ``` 181 | That's it. Open the index.html in src/SecureDiary folder and start writing! 182 | 183 |

184 | 185 |

186 | 187 | # License 188 | 189 | Published under the MIT License. This basically means the software is free and anyone can use it however they wish. No liability or warranty. 190 | 191 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE") 7 | } 8 | } 9 | 10 | plugins { 11 | id "org.sonarqube" version "2.5" 12 | } 13 | 14 | apply plugin: 'jacoco' 15 | apply plugin: 'java' 16 | apply plugin: 'pmd' 17 | pmd { 18 | toolVersion='5.5.2' 19 | ruleSets=["java-basic","java-imports","java-strings","java-clone","java-design"] 20 | } 21 | apply plugin: 'findbugs' 22 | findbugs { 23 | sourceSets = [sourceSets.main] 24 | toolVersion = "3.0.1" 25 | } 26 | 27 | 28 | apply plugin: 'eclipse' 29 | apply plugin: 'idea' 30 | 31 | group = 'com.github.maxamel' 32 | version = '0.0.1' 33 | 34 | apply plugin: 'io.spring.dependency-management' 35 | apply plugin: 'org.springframework.boot' 36 | 37 | repositories { 38 | mavenCentral() 39 | maven { url "https://oss.sonatype.org/content/groups/public" } 40 | } 41 | 42 | dependencies { 43 | compile('org.springframework.boot:spring-boot-devtools') 44 | compile('org.springframework.cloud:spring-cloud-starter-config') 45 | compile('org.springframework.boot:spring-boot-starter-actuator') 46 | compile('org.springframework.boot:spring-boot-starter-web') 47 | compile('org.springframework.boot:spring-boot-starter-data-jpa') 48 | compile('org.springframework.boot:spring-boot-starter-security') 49 | 50 | compile ('org.springframework.boot:spring-boot-starter-jdbc') 51 | compile ('org.springframework:spring-context') 52 | 53 | compile('com.github.rozidan:levelog-spring-boot:1.0.0-SNAPSHOT') 54 | compile('com.github.rozidan:modelmapper-spring-boot-starter:1.0.1-SNAPSHOT') 55 | compile('com.github.rozidan:logger-spring-boot:1.0.0-SNAPSHOT') 56 | compile('org.springframework.kafka:spring-kafka') 57 | compile('org.apache.kafka:kafka_2.11:1.0.0'){ 58 | exclude group: "org.slf4j" 59 | } 60 | compile('com.101tec:zkclient:0.7'){ 61 | exclude group: "org.slf4j" 62 | } 63 | compile('org.springframework.kafka:spring-kafka-test:2.1.2.RELEASE') 64 | compile('org.toile-libre.libe:curl:0.0.13') 65 | compile('org.mockito:mockito-all:1.10.19') 66 | 67 | 68 | compile('io.springfox:springfox-swagger2:2.7.0') 69 | compile('org.flywaydb:flyway-core') 70 | 71 | compileOnly('org.projectlombok:lombok') 72 | 73 | runtime('com.h2database:h2') 74 | 75 | testCompile('org.springframework.boot:spring-boot-starter-test') 76 | } 77 | 78 | sourceCompatibility = JavaVersion.VERSION_1_8 79 | 80 | jar { 81 | manifest { 82 | attributes("Implementation-Title": project.name, 83 | "Implementation-Version": project.version) 84 | } 85 | } 86 | 87 | dependencyManagement { 88 | imports { 89 | mavenBom("org.springframework.cloud:spring-cloud-dependencies:Edgware.RELEASE") 90 | } 91 | dependencies { 92 | dependency 'com.google.guava:guava:23.5-jre' 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/demo.png -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/diagram.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Feb 16 18:43:59 IST 2018 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.5.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/SecureDiary/css/alertify.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dimmer{position:fixed;z-index:1981;top:0;right:0;bottom:0;left:0;padding:0;margin:0;background-color:#252525;opacity:.5}.alertify .ajs-modal{position:fixed;top:0;right:0;left:0;bottom:0;padding:0;overflow-y:auto;z-index:1981}.alertify .ajs-dialog{position:relative;margin:5% auto;min-height:110px;max-width:500px;padding:24px 24px 0 24px;outline:0;background-color:#fff}.alertify .ajs-dialog.ajs-capture:before{content:'';position:absolute;top:0;right:0;bottom:0;left:0;display:block;z-index:1}.alertify .ajs-reset{position:absolute!important;display:inline!important;width:0!important;height:0!important;opacity:0!important}.alertify .ajs-commands{position:absolute;right:4px;margin:-14px 24px 0 0;z-index:2}.alertify .ajs-commands button{display:none;width:10px;height:10px;margin-left:10px;padding:10px;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.alertify .ajs-commands button.ajs-close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAh0lEQVQYlY2QsQ0EIQwEB9cBAR1CJUaI/gigDnwR6NBL/7/xWLNrZ2b8EwGotVpr7eOitWa1VjugiNB7R1UPrKrWe0dEAHBbXUqxMQbeewDmnHjvyTm7C3zDwAUd9c63YQdUVdu6EAJzzquz7HXvTiklt+H9DQFYaxFjvDqllFyMkbXWvfpXHjJrWFgdBq/hAAAAAElFTkSuQmCC)}.alertify .ajs-commands button.ajs-maximize{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAOUlEQVQYlWP8//8/AzGAhYGBgaG4uBiv6t7eXkYmooxjYGAgWiELsvHYFMCcRX2rSXcjoSBiJDbAAeD+EGu+8BZcAAAAAElFTkSuQmCC)}.alertify .ajs-header{margin:-24px;margin-bottom:0;padding:16px 24px;background-color:#fff}.alertify .ajs-body{min-height:56px}.alertify .ajs-body .ajs-content{padding:16px 24px 16px 16px}.alertify .ajs-footer{padding:4px;margin-left:-24px;margin-right:-24px;min-height:43px;background-color:#fff}.alertify .ajs-footer .ajs-buttons.ajs-primary{text-align:right}.alertify .ajs-footer .ajs-buttons.ajs-primary .ajs-button{margin:4px}.alertify .ajs-footer .ajs-buttons.ajs-auxiliary{float:left;clear:none;text-align:left}.alertify .ajs-footer .ajs-buttons.ajs-auxiliary .ajs-button{margin:4px}.alertify .ajs-footer .ajs-buttons .ajs-button{min-width:88px;min-height:35px}.alertify .ajs-handle{position:absolute;display:none;width:10px;height:10px;right:0;bottom:0;z-index:1;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMS8xNEDQYmMAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQ0lEQVQYlaXNMQoAIAxD0dT7H657l0KX3iJuUlBUNOsPPCGJm7VDp6ryeMxMuDsAQH7owW3pyn3RS26iKxERMLN3ugOaAkaL3sWVigAAAABJRU5ErkJggg==);-webkit-transform:scaleX(1);transform:scaleX(1);cursor:se-resize}.alertify.ajs-no-overflow .ajs-body .ajs-content{overflow:hidden!important}.alertify.ajs-no-padding.ajs-maximized .ajs-body .ajs-content{left:0;right:0;padding:0}.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body{margin-left:-24px;margin-right:-24px}.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body .ajs-content{padding:0}.alertify.ajs-no-padding.ajs-resizable .ajs-body .ajs-content{left:0;right:0}.alertify.ajs-maximizable .ajs-commands button.ajs-maximize,.alertify.ajs-maximizable .ajs-commands button.ajs-restore{display:inline-block}.alertify.ajs-closable .ajs-commands button.ajs-close{display:inline-block}.alertify.ajs-maximized .ajs-dialog{width:100%!important;height:100%!important;max-width:none!important;margin:0 auto!important;top:0!important;left:0!important}.alertify.ajs-maximized.ajs-modeless .ajs-modal{position:fixed!important;min-height:100%!important;max-height:none!important;margin:0!important}.alertify.ajs-maximized .ajs-commands button.ajs-maximize{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAASklEQVQYlZWQ0QkAMQhDtXRincOZX78KVtrDCwgqJNEoIB3MPLj7lRUROlpyVXGzby6zWuY+kz6tj5sBMTMAyVV3/595RbOh3cAXsww1raeiOcoAAAAASUVORK5CYII=)}.alertify.ajs-maximized .ajs-dialog,.alertify.ajs-resizable .ajs-dialog{padding:0}.alertify.ajs-maximized .ajs-commands,.alertify.ajs-resizable .ajs-commands{margin:14px 24px 0 0}.alertify.ajs-maximized .ajs-header,.alertify.ajs-resizable .ajs-header{position:absolute;top:0;left:0;right:0;margin:0;padding:16px 24px}.alertify.ajs-maximized .ajs-body,.alertify.ajs-resizable .ajs-body{min-height:224px;display:inline-block}.alertify.ajs-maximized .ajs-body .ajs-content,.alertify.ajs-resizable .ajs-body .ajs-content{position:absolute;top:50px;right:24px;bottom:50px;left:24px;overflow:auto}.alertify.ajs-maximized .ajs-footer,.alertify.ajs-resizable .ajs-footer{position:absolute;left:0;right:0;bottom:0;margin:0}.alertify.ajs-resizable:not(.ajs-maximized) .ajs-dialog{min-width:548px}.alertify.ajs-resizable:not(.ajs-maximized) .ajs-handle{display:block}.alertify.ajs-movable:not(.ajs-maximized) .ajs-header{cursor:move}.alertify.ajs-modeless .ajs-dimmer,.alertify.ajs-modeless .ajs-reset{display:none}.alertify.ajs-modeless .ajs-modal{overflow:visible;max-width:none;max-height:0}.alertify.ajs-modeless.ajs-pinnable .ajs-commands button.ajs-pin{display:inline-block;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQklEQVQYlcWPMQ4AIAwCqU9u38GbcbHRWN1MvKQDhQFMEpKImGJA0gCgnYw0V0rwxseg5erT4oSkQVI5d9f+e9+xA0NbLpWfitPXAAAAAElFTkSuQmCC)}.alertify.ajs-modeless.ajs-unpinned .ajs-modal{position:absolute}.alertify.ajs-modeless.ajs-unpinned .ajs-commands button.ajs-pin{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAO0lEQVQYlWP8//8/AzGAiShV6AqLi4txGs+CLoBLMYbC3t5eRmyaWfBZhwwYkX2NTxPRvibKjRhW4wMAhxkYGbLu3pEAAAAASUVORK5CYII=)}.alertify.ajs-modeless:not(.ajs-unpinned) .ajs-body{max-height:500px;overflow:auto}.alertify.ajs-basic .ajs-header{opacity:0}.alertify.ajs-basic .ajs-footer{visibility:hidden}.alertify.ajs-frameless .ajs-header{position:absolute;top:0;left:0;right:0;min-height:60px;margin:0;padding:0;opacity:0;z-index:1}.alertify.ajs-frameless .ajs-footer{display:none}.alertify.ajs-frameless .ajs-body .ajs-content{position:absolute;top:0;right:0;bottom:0;left:0}.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog{padding-top:0}.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog .ajs-commands{margin-top:0}.ajs-no-overflow{overflow:hidden!important;outline:0}.ajs-no-overflow.ajs-fixed{position:fixed;top:0;right:0;bottom:0;left:0;overflow-y:scroll!important}.ajs-no-selection,.ajs-no-selection *{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@media screen and (max-width:568px){.alertify .ajs-dialog{min-width:150px}.alertify:not(.ajs-maximized) .ajs-modal{padding:0 5%}.alertify:not(.ajs-maximized).ajs-resizable .ajs-dialog{min-width:initial;min-width:auto}}@-moz-document url-prefix(){.alertify button:focus{outline:1px dotted #3593d2}}.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);-webkit-transition-property:opacity,visibility;transition-property:opacity,visibility;-webkit-transition-timing-function:linear;transition-timing-function:linear;-webkit-transition-duration:250ms;transition-duration:250ms}.alertify.ajs-hidden .ajs-dimmer,.alertify.ajs-hidden .ajs-modal{visibility:hidden;opacity:0}.alertify.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-duration:.5s;animation-duration:.5s}.alertify.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-duration:250ms;animation-duration:250ms}.alertify .ajs-dialog.ajs-shake{-webkit-animation-name:ajs-shake;animation-name:ajs-shake;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}@-webkit-keyframes ajs-shake{0%,100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes ajs-shake{0%,100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.alertify.ajs-slide.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-slideIn;animation-name:ajs-slideIn;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1.275);animation-timing-function:cubic-bezier(.175,.885,.32,1.275)}.alertify.ajs-slide.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-slideOut;animation-name:ajs-slideOut;-webkit-animation-timing-function:cubic-bezier(.6,-.28,.735,.045);animation-timing-function:cubic-bezier(.6,-.28,.735,.045)}.alertify.ajs-zoom.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-zoomIn;animation-name:ajs-zoomIn}.alertify.ajs-zoom.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-zoomOut;animation-name:ajs-zoomOut}.alertify.ajs-fade.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-fadeIn;animation-name:ajs-fadeIn}.alertify.ajs-fade.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-fadeOut;animation-name:ajs-fadeOut}.alertify.ajs-pulse.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-pulseIn;animation-name:ajs-pulseIn}.alertify.ajs-pulse.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-pulseOut;animation-name:ajs-pulseOut}.alertify.ajs-flipx.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-flipInX;animation-name:ajs-flipInX}.alertify.ajs-flipx.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-flipOutX;animation-name:ajs-flipOutX}.alertify.ajs-flipy.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-flipInY;animation-name:ajs-flipInY}.alertify.ajs-flipy.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-flipOutY;animation-name:ajs-flipOutY}@-webkit-keyframes ajs-pulseIn{0%,100%,20%,40%,60%,80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes ajs-pulseIn{0%,100%,20%,40%,60%,80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes ajs-pulseOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes ajs-pulseOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@-webkit-keyframes ajs-zoomIn{0%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes ajs-zoomIn{0%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes ajs-zoomOut{0%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}100%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}}@keyframes ajs-zoomOut{0%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}100%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}}@-webkit-keyframes ajs-fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes ajs-fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes ajs-fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes ajs-fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes ajs-flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes ajs-flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes ajs-flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);opacity:0}}@keyframes ajs-flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);opacity:0}}@-webkit-keyframes ajs-flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-20deg);transform:perspective(400px) rotate3d(0,1,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,1,0,10deg);transform:perspective(400px) rotate3d(0,1,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-5deg);transform:perspective(400px) rotate3d(0,1,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes ajs-flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-20deg);transform:perspective(400px) rotate3d(0,1,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,1,0,10deg);transform:perspective(400px) rotate3d(0,1,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-5deg);transform:perspective(400px) rotate3d(0,1,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes ajs-flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-15deg);transform:perspective(400px) rotate3d(0,1,0,-15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);opacity:0}}@keyframes ajs-flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-15deg);transform:perspective(400px) rotate3d(0,1,0,-15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);opacity:0}}@-webkit-keyframes ajs-slideIn{0%{margin-top:-100%}100%{margin-top:5%}}@keyframes ajs-slideIn{0%{margin-top:-100%}100%{margin-top:5%}}@-webkit-keyframes ajs-slideOut{0%{margin-top:5%}100%{margin-top:-100%}}@keyframes ajs-slideOut{0%{margin-top:5%}100%{margin-top:-100%}}.alertify-notifier{position:fixed;width:0;overflow:visible;z-index:1982;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.alertify-notifier .ajs-message{position:relative;width:260px;max-height:0;padding:0;opacity:0;margin:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);-webkit-transition-duration:250ms;transition-duration:250ms;-webkit-transition-timing-function:linear;transition-timing-function:linear}.alertify-notifier .ajs-message.ajs-visible{-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.275);transition-timing-function:cubic-bezier(.175,.885,.32,1.275);opacity:1;max-height:100%;padding:15px;margin-top:10px}.alertify-notifier .ajs-message.ajs-success{background:rgba(91,189,114,.95)}.alertify-notifier .ajs-message.ajs-error{background:rgba(217,92,92,.95)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95)}.alertify-notifier .ajs-message .ajs-close{position:absolute;top:0;right:0;width:16px;height:16px;cursor:pointer;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABGdBTUEAALGPC/xhBQAAAFBJREFUGBl1j0EKADEIA+ve/P9f9bh1hEihNBfjVCO1v7RKVqJK4h8gM5cAPR42AkQEpSXPwMTyoi13n5N9YqJehm3Fnr7nL1D0ZEbD5OubGyC7a9gx+9eNAAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:center center;background-color:rgba(0,0,0,.5);border-top-right-radius:2px}.alertify-notifier.ajs-top{top:10px}.alertify-notifier.ajs-bottom{bottom:10px}.alertify-notifier.ajs-right{right:10px}.alertify-notifier.ajs-right .ajs-message{right:-320px}.alertify-notifier.ajs-right .ajs-message.ajs-visible{right:290px}.alertify-notifier.ajs-left{left:10px}.alertify-notifier.ajs-left .ajs-message{left:-300px}.alertify-notifier.ajs-left .ajs-message.ajs-visible{left:0}.alertify-notifier.ajs-center{left:50%}.alertify-notifier.ajs-center .ajs-message{-webkit-transform:translateX(-50%);transform:translateX(-50%)}.alertify-notifier.ajs-center .ajs-message.ajs-visible{left:50%;-webkit-transition-timing-function:cubic-bezier(.57,.43,.1,.65);transition-timing-function:cubic-bezier(.57,.43,.1,.65)}.alertify-notifier.ajs-center.ajs-top .ajs-message{top:-300px}.alertify-notifier.ajs-center.ajs-top .ajs-message.ajs-visible{top:0}.alertify-notifier.ajs-center.ajs-bottom .ajs-message{bottom:-300px}.alertify-notifier.ajs-center.ajs-bottom .ajs-message.ajs-visible{bottom:0} -------------------------------------------------------------------------------- /src/SecureDiary/css/bootstrap.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dimmer { 7 | background-color: #000; 8 | opacity: .5; 9 | } 10 | .alertify .ajs-dialog { 11 | max-width: 600px; 12 | min-height: 122px; 13 | background-color: #fff; 14 | border: 1px solid rgba(0, 0, 0, 0.2); 15 | -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); 16 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); 17 | border-radius: 6px; 18 | } 19 | .alertify .ajs-header { 20 | color: #333; 21 | border-bottom: 1px solid #e5e5e5; 22 | border-radius: 6px 6px 0 0; 23 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 24 | font-size: 18px; 25 | } 26 | .alertify .ajs-body { 27 | font-family: 'Roboto', sans-serif; 28 | color: black; 29 | } 30 | .alertify.ajs-resizable .ajs-content, 31 | .alertify.ajs-maximized:not(.ajs-resizable) .ajs-content { 32 | top: 58px; 33 | bottom: 68px; 34 | } 35 | .alertify .ajs-footer { 36 | background-color: #fff; 37 | padding: 15px; 38 | border-top: 1px solid #e5e5e5; 39 | border-radius: 0 0 6px 6px; 40 | } 41 | .alertify-notifier .ajs-message { 42 | background: rgba(255, 255, 255, 0.95); 43 | color: #000; 44 | text-align: center; 45 | border: solid 1px #ddd; 46 | border-radius: 2px; 47 | } 48 | .alertify-notifier .ajs-message.ajs-success { 49 | color: #fff; 50 | background: rgba(91, 189, 114, 0.95); 51 | text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); 52 | } 53 | .alertify-notifier .ajs-message.ajs-error { 54 | color: #fff; 55 | background: rgba(217, 92, 92, 0.95); 56 | text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); 57 | } 58 | .alertify-notifier .ajs-message.ajs-warning { 59 | background: rgba(252, 248, 215, 0.95); 60 | border-color: #999; 61 | } 62 | -------------------------------------------------------------------------------- /src/SecureDiary/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dimmer{background-color:#000;opacity:.5}.alertify .ajs-dialog{max-width:600px;min-height:122px;background-color:#fff;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5);border-radius:6px}.alertify .ajs-header{color:#333;border-bottom:1px solid #e5e5e5;border-radius:6px 6px 0 0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:18px}.alertify .ajs-body{font-family:Roboto,sans-serif;color:#000}.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content,.alertify.ajs-resizable .ajs-content{top:58px;bottom:68px}.alertify .ajs-footer{background-color:#fff;padding:15px;border-top:1px solid #e5e5e5;border-radius:0 0 6px 6px}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:solid 1px #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} -------------------------------------------------------------------------------- /src/SecureDiary/css/bootstrap.rtl.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dimmer{background-color:#000;opacity:.5}.alertify .ajs-dialog{max-width:600px;min-height:122px;background-color:#fff;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5);border-radius:6px}.alertify .ajs-header{color:#333;border-bottom:1px solid #e5e5e5;border-radius:6px 6px 0 0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:18px}.alertify .ajs-body{font-family:Roboto,sans-serif;color:#000}.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content,.alertify.ajs-resizable .ajs-content{top:58px;bottom:68px}.alertify .ajs-footer{background-color:#fff;padding:15px;border-top:1px solid #e5e5e5;border-radius:0 0 6px 6px}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:solid 1px #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} -------------------------------------------------------------------------------- /src/SecureDiary/css/default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dialog { 7 | background-color: white; 8 | -webkit-box-shadow: 0px 15px 20px 0px rgba(0, 0, 0, 0.25); 9 | box-shadow: 0px 15px 20px 0px rgba(0, 0, 0, 0.25); 10 | border-radius: 2px; 11 | } 12 | .alertify .ajs-header { 13 | color: black; 14 | font-weight: bold; 15 | background: #fafafa; 16 | border-bottom: #eee 1px solid; 17 | border-radius: 2px 2px 0 0; 18 | } 19 | .alertify .ajs-body { 20 | color: black; 21 | } 22 | .alertify .ajs-body .ajs-content .ajs-input { 23 | display: block; 24 | width: 100%; 25 | padding: 8px; 26 | margin: 4px; 27 | border-radius: 2px; 28 | border: 1px solid #CCC; 29 | } 30 | .alertify .ajs-body .ajs-content p { 31 | margin: 0; 32 | } 33 | .alertify .ajs-footer { 34 | background: #fbfbfb; 35 | border-top: #eee 1px solid; 36 | border-radius: 0 0 2px 2px; 37 | } 38 | .alertify .ajs-footer .ajs-buttons .ajs-button { 39 | background-color: transparent; 40 | color: #000; 41 | border: 0; 42 | font-size: 14px; 43 | font-weight: bold; 44 | text-transform: uppercase; 45 | } 46 | .alertify .ajs-footer .ajs-buttons .ajs-button.ajs-ok { 47 | color: #3593D2; 48 | } 49 | .alertify-notifier .ajs-message { 50 | background: rgba(255, 255, 255, 0.95); 51 | color: #000; 52 | text-align: center; 53 | border: solid 1px #ddd; 54 | border-radius: 2px; 55 | } 56 | .alertify-notifier .ajs-message.ajs-success { 57 | color: #fff; 58 | background: rgba(91, 189, 114, 0.95); 59 | text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); 60 | } 61 | .alertify-notifier .ajs-message.ajs-error { 62 | color: #fff; 63 | background: rgba(217, 92, 92, 0.95); 64 | text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); 65 | } 66 | .alertify-notifier .ajs-message.ajs-warning { 67 | background: rgba(252, 248, 215, 0.95); 68 | border-color: #999; 69 | } 70 | -------------------------------------------------------------------------------- /src/SecureDiary/css/default.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dialog{background-color:#fff;-webkit-box-shadow:0 15px 20px 0 rgba(0,0,0,.25);box-shadow:0 15px 20px 0 rgba(0,0,0,.25);border-radius:2px}.alertify .ajs-header{color:#000;font-weight:700;background:#fafafa;border-bottom:#eee 1px solid;border-radius:2px 2px 0 0}.alertify .ajs-body{color:#000}.alertify .ajs-body .ajs-content .ajs-input{display:block;width:100%;padding:8px;margin:4px;border-radius:2px;border:1px solid #ccc}.alertify .ajs-body .ajs-content p{margin:0}.alertify .ajs-footer{background:#fbfbfb;border-top:#eee 1px solid;border-radius:0 0 2px 2px}.alertify .ajs-footer .ajs-buttons .ajs-button{background-color:transparent;color:#000;border:0;font-size:14px;font-weight:700;text-transform:uppercase}.alertify .ajs-footer .ajs-buttons .ajs-button.ajs-ok{color:#3593d2}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:solid 1px #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} -------------------------------------------------------------------------------- /src/SecureDiary/css/default.rtl.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dialog{background-color:#fff;-webkit-box-shadow:0 15px 20px 0 rgba(0,0,0,.25);box-shadow:0 15px 20px 0 rgba(0,0,0,.25);border-radius:2px}.alertify .ajs-header{color:#000;font-weight:700;background:#fafafa;border-bottom:#eee 1px solid;border-radius:2px 2px 0 0}.alertify .ajs-body{color:#000}.alertify .ajs-body .ajs-content .ajs-input{display:block;width:100%;padding:8px;margin:4px;border-radius:2px;border:1px solid #ccc}.alertify .ajs-body .ajs-content p{margin:0}.alertify .ajs-footer{background:#fbfbfb;border-top:#eee 1px solid;border-radius:0 0 2px 2px}.alertify .ajs-footer .ajs-buttons .ajs-button{background-color:transparent;color:#000;border:0;font-size:14px;font-weight:700;text-transform:uppercase}.alertify .ajs-footer .ajs-buttons .ajs-button.ajs-ok{color:#3593d2}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:solid 1px #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} -------------------------------------------------------------------------------- /src/SecureDiary/css/semantic.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dimmer { 7 | background-color: rgba(0, 0, 0, 0.85); 8 | opacity: 1; 9 | } 10 | .alertify .ajs-dialog { 11 | max-width: 50%; 12 | min-height: 137px; 13 | background-color: #F4F4F4; 14 | border: 1px solid #DDD; 15 | -webkit-box-shadow: none; 16 | box-shadow: none; 17 | border-radius: 5px; 18 | } 19 | .alertify .ajs-header { 20 | padding: 1.5rem 2rem; 21 | border-bottom: none; 22 | border-radius: 5px 5px 0 0; 23 | color: #555; 24 | background-color: #fff; 25 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 26 | font-size: 1.6em; 27 | font-weight: 700; 28 | } 29 | .alertify .ajs-body { 30 | font-family: 'Roboto', sans-serif; 31 | color: #555; 32 | } 33 | .alertify .ajs-body .ajs-content .ajs-input { 34 | width: 100%; 35 | margin: 0; 36 | padding: .65em 1em; 37 | font-size: 1em; 38 | background-color: #FFF; 39 | border: 1px solid rgba(0, 0, 0, 0.15); 40 | outline: 0; 41 | color: rgba(0, 0, 0, 0.7); 42 | border-radius: .3125em; 43 | -webkit-transition: background-color 0.3s ease-out, border-color 0.2s ease, -webkit-box-shadow 0.2s ease; 44 | transition: background-color 0.3s ease-out, border-color 0.2s ease, -webkit-box-shadow 0.2s ease; 45 | transition: background-color 0.3s ease-out, box-shadow 0.2s ease, border-color 0.2s ease; 46 | transition: background-color 0.3s ease-out, box-shadow 0.2s ease, border-color 0.2s ease, -webkit-box-shadow 0.2s ease; 47 | -webkit-box-sizing: border-box; 48 | box-sizing: border-box; 49 | } 50 | .alertify .ajs-body .ajs-content .ajs-input:active { 51 | border-color: rgba(0, 0, 0, 0.3); 52 | background-color: #FAFAFA; 53 | } 54 | .alertify .ajs-body .ajs-content .ajs-input:focus { 55 | border-color: rgba(0, 0, 0, 0.2); 56 | color: rgba(0, 0, 0, 0.85); 57 | } 58 | .alertify.ajs-resizable .ajs-content, 59 | .alertify.ajs-maximized:not(.ajs-resizable) .ajs-content { 60 | top: 64px; 61 | bottom: 74px; 62 | } 63 | .alertify .ajs-footer { 64 | background-color: #fff; 65 | padding: 1rem 2rem; 66 | border-top: none; 67 | border-radius: 0 0 5px 5px; 68 | } 69 | .alertify-notifier .ajs-message { 70 | background: rgba(255, 255, 255, 0.95); 71 | color: #000; 72 | text-align: center; 73 | border: solid 1px #ddd; 74 | border-radius: 2px; 75 | } 76 | .alertify-notifier .ajs-message.ajs-success { 77 | color: #fff; 78 | background: rgba(91, 189, 114, 0.95); 79 | text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); 80 | } 81 | .alertify-notifier .ajs-message.ajs-error { 82 | color: #fff; 83 | background: rgba(217, 92, 92, 0.95); 84 | text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); 85 | } 86 | .alertify-notifier .ajs-message.ajs-warning { 87 | background: rgba(252, 248, 215, 0.95); 88 | border-color: #999; 89 | } 90 | -------------------------------------------------------------------------------- /src/SecureDiary/css/semantic.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dimmer{background-color:rgba(0,0,0,.85);opacity:1}.alertify .ajs-dialog{max-width:50%;min-height:137px;background-color:#f4f4f4;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none;border-radius:5px}.alertify .ajs-header{padding:1.5rem 2rem;border-bottom:none;border-radius:5px 5px 0 0;color:#555;background-color:#fff;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:1.6em;font-weight:700}.alertify .ajs-body{font-family:Roboto,sans-serif;color:#555}.alertify .ajs-body .ajs-content .ajs-input{width:100%;margin:0;padding:.65em 1em;font-size:1em;background-color:#fff;border:1px solid rgba(0,0,0,.15);outline:0;color:rgba(0,0,0,.7);border-radius:.3125em;-webkit-transition:background-color .3s ease-out,border-color .2s ease,-webkit-box-shadow .2s ease;transition:background-color .3s ease-out,border-color .2s ease,-webkit-box-shadow .2s ease;transition:background-color .3s ease-out,box-shadow .2s ease,border-color .2s ease;transition:background-color .3s ease-out,box-shadow .2s ease,border-color .2s ease,-webkit-box-shadow .2s ease;-webkit-box-sizing:border-box;box-sizing:border-box}.alertify .ajs-body .ajs-content .ajs-input:active{border-color:rgba(0,0,0,.3);background-color:#fafafa}.alertify .ajs-body .ajs-content .ajs-input:focus{border-color:rgba(0,0,0,.2);color:rgba(0,0,0,.85)}.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content,.alertify.ajs-resizable .ajs-content{top:64px;bottom:74px}.alertify .ajs-footer{background-color:#fff;padding:1rem 2rem;border-top:none;border-radius:0 0 5px 5px}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:solid 1px #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} -------------------------------------------------------------------------------- /src/SecureDiary/css/semantic.rtl.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * alertifyjs 1.11.0 http://alertifyjs.com 3 | * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. 4 | * Copyright 2017 Mohammad Younes (http://alertifyjs.com) 5 | * Licensed under GPL 3 */ 6 | .alertify .ajs-dimmer{background-color:rgba(0,0,0,.85);opacity:1}.alertify .ajs-dialog{max-width:50%;min-height:137px;background-color:#f4f4f4;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none;border-radius:5px}.alertify .ajs-header{padding:1.5rem 2rem;border-bottom:none;border-radius:5px 5px 0 0;color:#555;background-color:#fff;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:1.6em;font-weight:700}.alertify .ajs-body{font-family:Roboto,sans-serif;color:#555}.alertify .ajs-body .ajs-content .ajs-input{width:100%;margin:0;padding:.65em 1em;font-size:1em;background-color:#fff;border:1px solid rgba(0,0,0,.15);outline:0;color:rgba(0,0,0,.7);border-radius:.3125em;-webkit-transition:background-color .3s ease-out,border-color .2s ease,-webkit-box-shadow .2s ease;transition:background-color .3s ease-out,border-color .2s ease,-webkit-box-shadow .2s ease;transition:background-color .3s ease-out,box-shadow .2s ease,border-color .2s ease;transition:background-color .3s ease-out,box-shadow .2s ease,border-color .2s ease,-webkit-box-shadow .2s ease;-webkit-box-sizing:border-box;box-sizing:border-box}.alertify .ajs-body .ajs-content .ajs-input:active{border-color:rgba(0,0,0,.3);background-color:#fafafa}.alertify .ajs-body .ajs-content .ajs-input:focus{border-color:rgba(0,0,0,.2);color:rgba(0,0,0,.85)}.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content,.alertify.ajs-resizable .ajs-content{top:64px;bottom:74px}.alertify .ajs-footer{background-color:#fff;padding:1rem 2rem;border-top:none;border-radius:0 0 5px 5px}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:solid 1px #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} -------------------------------------------------------------------------------- /src/SecureDiary/css/styles.css: -------------------------------------------------------------------------------- 1 | body, article, h1, h2, ul, li { margin:0; padding:0; } 2 | ul, li { list-style-type:none; } 3 | body { font-family:sans-serif; padding:0; margin:0; background-color:#FFF; } 4 | h1, article h2 { cursor:pointer; } 5 | 6 | h1 { font-size:1.4em; padding:.45em; text-align:center; color:#FFF; text-shadow:2px 2px 2px rgba(0,0,0,.5); 7 | background:#f4e842; 8 | background:-moz-linear-gradient(top, #f4e842 2%, #607D27 100%); 9 | background:-webkit-gradient(linear, left top, left bottom, color-stop(2%,#f4e842), color-stop(100%,#607D27)); 10 | background:-webkit-linear-gradient(top, #f4e842 2%,#607D27 100%); 11 | background:-o-linear-gradient(top, #f4e842 2%,#607D27 100%); 12 | background:-ms-linear-gradient(top, #f4e842 2%,#607D27 100%); 13 | background:linear-gradient(top, #f4e842 2%,#607D27 100%); 14 | } 15 | 16 | article { border-bottom:1px solid #CCC; border-top:1px solid #FFF; font-size:1em; } 17 | article h2 { padding:.8em .7em; font-size:1.1em; background-color:#EAEDF4; transition:background-color .4s; -moz-transition:background-color .4s; -webkit-transition:background-color .4s; -o-transition:background-color .4s; } 18 | article h2 img { display:block; float:right; margin-left:.8em; cursor:pointer; } 19 | article.sel h2 { background-color:#E01B6A; color:#FFF; text-shadow:2px 2px 2px rgba(0,0,0,.5); } 20 | article p { max-height:0; overflow:hidden; padding:0 .1em 0 .7em; margin:0; transition:all .5s; -moz-transition:all .5s; -webkit-transition:all .5s; -o-transition:all .5s;} 21 | article.sel p { max-height:15em; padding:.7em .1em .7em .7em; } 22 | article p .actions { float: right; width:30px; margin-left:.6em;} 23 | article p .actions img { display:block; width:20px; height:20px; cursor:pointer; margin:.6em 0; } 24 | article p .time { display:block; font-size:.8em; color:#968F92; margin-top: .6em; } 25 | article p .map { display: block; font-size:.8em; color:#1B7AE0; font-style:italic; margin-top:.4em; text-decoration:none;} 26 | article p .map:hover { text-decoration:underline; } 27 | 28 | #status { display: block; cursor:pointer; position:absolute; top: .40em; left:0.5em; background-color: transparent;} 29 | #refresh { display: block; cursor:pointer; position:absolute; top: .40em; right:3.5em; background-color: transparent;} 30 | #newEntry { display: block; cursor:pointer; position:absolute; top: .40em; right:0.5em } 31 | #remove { display: block; cursor:pointer; position:absolute; top: .40em; right:6.5em } 32 | #register { display: block; cursor:pointer; position:absolute; top: .40em; right:9.5em } 33 | 34 | #storeEntry { display:none; width:96%; padding: 2%; } 35 | #storeEntry label { display: block; width:100%; margin:.7em 0 .1em 0; font-weight:bold; } 36 | #storeEntry input[type=text], #storeEntry textarea { width:100%; border:1px solid #CCC; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } 37 | #storeEntry .error { border: 1px solid #C00!important; } 38 | #storeEntry #btnCancel, #storeEntry #btnSubmit { font-size:1.1em; } 39 | 40 | #noEntries { text-align:center; font-size:1.2em; } 41 | 42 | #location { font-size:.8em; margin-top: 1em; color: #968F92; font-style:italic; } 43 | #location a { color: #968F92; } 44 | 45 | form { 46 | /* Just to center the form on the page */ 47 | margin: 0 auto; 48 | width: 300px; 49 | align: center; 50 | /* To see the outline of the form */ 51 | padding: 24px 24px 24px 24px; 52 | border-radius: 1em; 53 | font-family: 'Roboto', sans-serif; 54 | } 55 | 56 | form div + div { 57 | margin-top: 1em; 58 | } 59 | 60 | label { 61 | /* To make sure that all labels have the same size and are properly aligned */ 62 | display: inline-block; 63 | width: 90px; 64 | text-align: center; 65 | } 66 | 67 | input, textarea { 68 | /* To make sure that all text fields have the same font settings 69 | By default, textareas have a monospace font */ 70 | font: 1em sans-serif; 71 | 72 | /* To give the same size to all text fields */ 73 | width: 300px; 74 | box-sizing: border-box; 75 | 76 | /* To harmonize the look & feel of text field border */ 77 | border: 1px solid #999; 78 | } 79 | 80 | input:focus, textarea:focus { 81 | /* To give a little highlight on active elements */ 82 | border-color: #000; 83 | } 84 | 85 | textarea { 86 | /* To properly align multiline text fields with their labels */ 87 | vertical-align: top; 88 | 89 | /* To give enough room to type some text */ 90 | height: 5em; 91 | } 92 | 93 | .button { 94 | /* To position the buttons to the same position of the text fields */ 95 | padding-left: 90px; /* same size as the label elements */ 96 | padding-top: 90px; 97 | } 98 | 99 | button { 100 | /* This extra margin represent roughly the same space as the space 101 | between the labels and their text fields */ 102 | margin-left: .5em; 103 | } -------------------------------------------------------------------------------- /src/SecureDiary/img/i_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/img/i_add.png -------------------------------------------------------------------------------- /src/SecureDiary/img/i_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/img/i_delete.png -------------------------------------------------------------------------------- /src/SecureDiary/img/i_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/img/i_edit.png -------------------------------------------------------------------------------- /src/SecureDiary/img/i_offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/img/i_offline.png -------------------------------------------------------------------------------- /src/SecureDiary/img/i_online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/img/i_online.png -------------------------------------------------------------------------------- /src/SecureDiary/img/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/img/refresh.png -------------------------------------------------------------------------------- /src/SecureDiary/img/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/img/register.png -------------------------------------------------------------------------------- /src/SecureDiary/img/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/img/remove.png -------------------------------------------------------------------------------- /src/SecureDiary/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | My Diary 15 | 16 | 17 | 18 |

Secure Diary Entries

19 |
20 |
21 |
    22 |
  • 23 | 24 | 25 |
  • 26 |
  • 27 | 28 | 29 |
  • 30 |
31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/SecureDiary/js/html5.js: -------------------------------------------------------------------------------- 1 | /*! HTML5 Shiv vpre3.6 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 2 | Uncompressed source: https://github.com/aFarkas/html5shiv */ 3 | (function(a,b){function h(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function i(){var a=l.elements;return typeof a=="string"?a.split(" "):a}function j(a){var b={},c=a.createElement,f=a.createDocumentFragment,g=f();a.createElement=function(a){if(!l.shivMethods)return c(a);var f;return b[a]?f=b[a].cloneNode():e.test(a)?f=(b[a]=c(a)).cloneNode():f=c(a),f.canHaveChildren&&!d.test(a)?g.appendChild(f):f},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+i().join().replace(/\w+/g,function(a){return c(a),g.createElement(a),'c("'+a+'")'})+");return n}")(l,g)}function k(a){var b;return a.documentShived?a:(l.shivCSS&&!f&&(b=!!h(a,"article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio{display:none}canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}mark{background:#FF0;color:#000}")),g||(b=!j(a)),b&&(a.documentShived=b),a)}var c=a.html5||{},d=/^<|^(?:button|form|map|select|textarea|object|iframe|option|optgroup)$/i,e=/^<|^(?:a|b|button|code|div|fieldset|form|h1|h2|h3|h4|h5|h6|i|iframe|img|input|label|li|link|ol|option|p|param|q|script|select|span|strong|style|table|tbody|td|textarea|tfoot|th|thead|tr|ul)$/i,f,g;(function(){var c=b.createElement("a");c.innerHTML="",f="hidden"in c,f&&typeof injectElementWithStyles=="function"&&injectElementWithStyles("#modernizr{}",function(b){b.hidden=!0,f=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle).display=="none"}),g=c.childNodes.length==1||function(){try{b.createElement("a")}catch(a){return!0}var c=b.createDocumentFragment();return typeof c.cloneNode=="undefined"||typeof c.createDocumentFragment=="undefined"||typeof c.createElement=="undefined"}()})();var l={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:k};a.html5=l,k(b)})(this,document) -------------------------------------------------------------------------------- /src/SecureDiary/js/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.2.0 Copyright jQuery Foundation and other contributors. 3 | Released under MIT license, http://github.com/requirejs/requirejs/LICENSE 4 | */ 5 | var requirejs,require,define; 6 | (function(ga){function ka(b,c,d,g){return g||""}function K(b){return"[object Function]"===Q.call(b)}function L(b){return"[object Array]"===Q.call(b)}function y(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(K(k)){if(this.events.error&&this.map.isDefine||g.onError!== 18 | ha)try{h=l.execCb(c,k,b,h)}catch(d){a=d}else h=l.execCb(c,k,b,h);this.map.isDefine&&void 0===h&&((b=this.module)?h=b.exports:this.usingExports&&(h=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",A(this.error=a)}else h=k;this.exports=h;if(this.map.isDefine&&!this.ignore&&(v[c]=h,g.onResourceLoad)){var f=[];y(this.depMaps,function(a){f.push(a.normalizedMap||a)});g.onResourceLoad(l,this.map,f)}C(c); 19 | this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);w(d,"defined",z(this,function(h){var k,f,d=e(fa,this.map.id),M=this.map.name,r=this.map.parentMap?this.map.parentMap.name:null,m=l.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(h.normalize&&(M=h.normalize(M,function(a){return c(a,r,!0)})|| 20 | ""),f=q(a.prefix+"!"+M,this.map.parentMap),w(f,"defined",z(this,function(a){this.map.normalizedMap=f;this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),h=e(t,f.id)){this.depMaps.push(f);if(this.events.error)h.on("error",z(this,function(a){this.emit("error",a)}));h.enable()}}else d?(this.map.url=l.nameToUrl(d),this.load()):(k=z(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),k.error=z(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];D(t,function(a){0=== 21 | a.map.id.indexOf(b+"_unnormalized")&&C(a.map.id)});A(a)}),k.fromText=z(this,function(h,c){var d=a.name,f=q(d),M=S;c&&(h=c);M&&(S=!1);u(f);x(p.config,b)&&(p.config[d]=p.config[b]);try{g.exec(h)}catch(e){return A(F("fromtexteval","fromText eval for "+b+" failed: "+e,e,[b]))}M&&(S=!0);this.depMaps.push(f);l.completeLoad(d);m([d],k)}),h.load(a.name,m,k,p))}));l.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){Z[this.map.id]=this;this.enabling=this.enabled=!0;y(this.depMaps,z(this,function(a, 22 | b){var c,h;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=e(R,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;w(a,"defined",z(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?w(a,"error",z(this,this.errback)):this.events.error&&w(a,"error",z(this,function(a){this.emit("error",a)}))}c=a.id;h=t[c];x(R,c)||!h||h.enabled||l.enable(a,this)}));D(this.pluginMaps,z(this,function(a){var b=e(t,a.id); 23 | b&&!b.enabled&&l.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){y(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};l={config:p,contextName:b,registry:t,defined:v,urlFetched:W,defQueue:G,defQueueMap:{},Module:da,makeModuleMap:q,nextTick:g.nextTick,onError:A,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");if("string"===typeof a.urlArgs){var b= 24 | a.urlArgs;a.urlArgs=function(a,c){return(-1===c.indexOf("?")?"?":"&")+b}}var c=p.shim,h={paths:!0,bundles:!0,config:!0,map:!0};D(a,function(a,b){h[b]?(p[b]||(p[b]={}),Y(p[b],a,!0,!0)):p[b]=a});a.bundles&&D(a.bundles,function(a,b){y(a,function(a){a!==b&&(fa[a]=b)})});a.shim&&(D(a.shim,function(a,b){L(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=l.makeShimExports(a));c[b]=a}),p.shim=c);a.packages&&y(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&& 25 | (p.paths[b]=a.location);p.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(U,"")});D(t,function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&l.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ga,arguments));return b||a.exports&&ia(a.exports)}},makeRequire:function(a,n){function m(c,d,f){var e,r;n.enableBuildCallback&&d&&K(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(K(d))return A(F("requireargs", 26 | "Invalid require call"),f);if(a&&x(R,c))return R[c](t[a.id]);if(g.get)return g.get(l,c,a,m);e=q(c,a,!1,!0);e=e.id;return x(v,e)?v[e]:A(F("notloaded",'Module name "'+e+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}P();l.nextTick(function(){P();r=u(q(null,a));r.skipMap=n.skipMap;r.init(c,d,f,{enabled:!0});H()});return m}n=n||{};Y(m,{isBrowser:E,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];-1!==f&&("."!==g&&".."!==g||1e.attachEvent.toString().indexOf("[native code")||ca?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(S=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;if(m.onNodeCreated)m.onNodeCreated(e,m,c,d);P=e;H?C.insertBefore(e,H):C.appendChild(e);P=null;return e}if(ja)try{setTimeout(function(){}, 35 | 0),importScripts(d),b.completeLoad(c)}catch(q){b.onError(F("importscripts","importScripts failed for "+c+" at "+d,q,[c]))}};E&&!w.skipDataMain&&X(document.getElementsByTagName("script"),function(b){C||(C=b.parentNode);if(O=b.getAttribute("data-main"))return u=O,w.baseUrl||-1!==u.indexOf("!")||(I=u.split("/"),u=I.pop(),T=I.length?I.join("/")+"/":"./",w.baseUrl=T),u=u.replace(U,""),g.jsExtRegExp.test(u)&&(u=O),w.deps=w.deps?w.deps.concat(u):[u],!0});define=function(b,c,d){var e,g;"string"!==typeof b&& 36 | (d=c,c=b,b=null);L(c)||(d=c,c=null);!c&&K(d)&&(c=[],d.length&&(d.toString().replace(qa,ka).replace(ra,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));S&&(e=P||pa())&&(b||(b=e.getAttribute("data-requiremodule")),g=J[e.getAttribute("data-requirecontext")]);g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):V.push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(w)}})(this); 37 | -------------------------------------------------------------------------------- /src/SecureDiary/screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/screenshots/screenshot1.png -------------------------------------------------------------------------------- /src/SecureDiary/screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/screenshots/screenshot2.png -------------------------------------------------------------------------------- /src/SecureDiary/screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/screenshots/screenshot3.png -------------------------------------------------------------------------------- /src/SecureDiary/screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/SecureDiary/screenshots/screenshot4.png -------------------------------------------------------------------------------- /src/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxamel/SpringZKAuth/03a3bc40849c677f3afe685b66a9acd3db17c7b3/src/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 15 12:11:51 IDT 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.4-all.zip 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/EnumUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | /** 6 | * @author Idan Rozenfeld 7 | */ 8 | @UtilityClass 9 | public class EnumUtils { 10 | 11 | public & IdentifierType> E getByValue(final Class enumClass, T value) { 12 | for (final E e : enumClass.getEnumConstants()) { 13 | if (e.getValue().equals(value)) { 14 | return e; 15 | } 16 | } 17 | 18 | throw new IllegalArgumentException("Wrong value " + value.toString() + " for type " + enumClass.getName()); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/IdentifierType.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | /** 6 | * @author Idan Rozenfeld 7 | */ 8 | @FunctionalInterface 9 | public interface IdentifierType { 10 | @JsonValue 11 | V getValue(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/ZKAuthApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SuppressWarnings("PMD") 7 | @SpringBootApplication 8 | public class ZKAuthApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(ZKAuthApplication.class, args); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/config/KafkaConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.config; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.apache.kafka.clients.producer.ProducerConfig; 7 | import org.apache.kafka.common.serialization.StringSerializer; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.kafka.annotation.EnableKafka; 12 | import org.springframework.kafka.core.*; 13 | import org.springframework.kafka.support.serializer.JsonSerializer; 14 | 15 | import com.github.maxamel.server.web.dtos.ChallengeDto; 16 | 17 | @EnableKafka 18 | @Configuration 19 | public class KafkaConfig { 20 | 21 | @Value("${kafka.broker.url}") 22 | private String brokerAddress; 23 | 24 | @Bean 25 | public Map producerConfigs() { 26 | Map props = new HashMap<>(); 27 | props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerAddress); 28 | props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 29 | props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); 30 | return props; 31 | } 32 | 33 | @Bean 34 | public ProducerFactory producerFactory() { 35 | return new DefaultKafkaProducerFactory<>(producerConfigs()); 36 | } 37 | 38 | @Bean 39 | public KafkaTemplate kafkaTemplate() { 40 | return new KafkaTemplate<>(producerFactory()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/config/LoggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.config; 2 | 3 | import com.github.rozidan.springboot.logger.EnableLogger; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @EnableLogger 8 | public class LoggerConfig { 9 | 10 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/model/AuditableEntity.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.model; 2 | 3 | import javax.persistence.EntityListeners; 4 | import javax.persistence.MappedSuperclass; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.data.annotation.CreatedBy; 8 | import org.springframework.data.annotation.LastModifiedBy; 9 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 10 | 11 | @Getter 12 | @NoArgsConstructor 13 | @MappedSuperclass 14 | @EntityListeners(AuditingEntityListener.class) 15 | public class AuditableEntity { 16 | @CreatedBy 17 | private String createdBy; 18 | 19 | 20 | @LastModifiedBy 21 | private String lastModifiedBy; 22 | 23 | 24 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/model/Constants.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.model; 2 | 3 | public final class Constants { 4 | 5 | public static final int TIMER_CHALLENGE = 0; 6 | 7 | public static final int TIMER_INACTIVITY = 1; 8 | 9 | private Constants(){} 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/model/Diary.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.model; 2 | 3 | import com.github.maxamel.server.domain.model.constraints.UserEntryNameUnique; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.SequenceGenerator; 11 | import javax.persistence.Table; 12 | import javax.persistence.UniqueConstraint; 13 | import lombok.AllArgsConstructor; 14 | import lombok.Builder; 15 | import lombok.Getter; 16 | import lombok.NoArgsConstructor; 17 | import lombok.Setter; 18 | import org.hibernate.validator.constraints.NotEmpty; 19 | 20 | /** 21 | * @author Max Amelchenko 22 | */ 23 | @Getter 24 | @Setter 25 | @AllArgsConstructor 26 | @NoArgsConstructor 27 | @Builder 28 | @Entity 29 | @Table(uniqueConstraints = 30 | {@UniqueConstraint(name = UserEntryNameUnique.CONSTRAINT_NAME, columnNames = {UserEntryNameUnique.USER_NAME, UserEntryNameUnique.ENTRY_NAME})}) 31 | @SequenceGenerator(name = "diarySeq", sequenceName = "diary_seq") 32 | public class Diary extends AuditableEntity { 33 | 34 | @Id 35 | @GeneratedValue(generator = "diarySeq", strategy = GenerationType.SEQUENCE) 36 | private Long id; 37 | 38 | @NotEmpty 39 | @Column(nullable = false, name = UserEntryNameUnique.USER_NAME) 40 | private String username; 41 | 42 | @NotEmpty 43 | @Column(nullable = false, name = UserEntryNameUnique.ENTRY_NAME) 44 | private String entryname; 45 | 46 | @NotEmpty 47 | @Column(nullable = false, length = 600) 48 | private String content; 49 | 50 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/model/User.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.model; 2 | 3 | import com.github.maxamel.server.domain.model.constraints.UserNameUnique; 4 | import com.github.maxamel.server.domain.model.types.SessionStatus; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.SequenceGenerator; 12 | import javax.persistence.Table; 13 | import javax.persistence.UniqueConstraint; 14 | import javax.validation.constraints.NotNull; 15 | import lombok.AllArgsConstructor; 16 | import lombok.Builder; 17 | import lombok.Getter; 18 | import lombok.NoArgsConstructor; 19 | import lombok.Setter; 20 | import org.hibernate.validator.constraints.NotEmpty; 21 | 22 | /** 23 | * @author Max Amelchenko 24 | */ 25 | @Getter 26 | @Setter 27 | @AllArgsConstructor 28 | @NoArgsConstructor 29 | @Builder 30 | @Entity 31 | @Table(uniqueConstraints = 32 | {@UniqueConstraint(name = UserNameUnique.CONSTRAINT_NAME, columnNames = UserNameUnique.FIELD_NAME)}) 33 | @SequenceGenerator(name = "userSeq", sequenceName = "user_seq") 34 | public class User extends AuditableEntity { 35 | 36 | @Id 37 | @GeneratedValue(generator = "userSeq", strategy = GenerationType.SEQUENCE) 38 | private Long id; 39 | 40 | @NotEmpty 41 | @Column(nullable = false, name = UserNameUnique.FIELD_NAME) 42 | private String name; 43 | 44 | @NotNull 45 | @Column(nullable = false, length = 600) 46 | private String passwordless; 47 | 48 | @Column(nullable = true, length = 600) 49 | private String secret; 50 | 51 | @Column 52 | private SessionStatus sstatus; 53 | 54 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/model/constraints/DataUniqueConstraint.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.model.constraints; 2 | 3 | public interface DataUniqueConstraint { 4 | 5 | String getConstraintName(); 6 | 7 | String[] getFieldNames(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/model/constraints/UserEntryNameUnique.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.model.constraints; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | @Component 6 | public class UserEntryNameUnique implements DataUniqueConstraint { 7 | 8 | public static final String CONSTRAINT_NAME = "UNIQUE_USERENTRY_NAME"; 9 | public static final String USER_NAME = "username"; 10 | public static final String ENTRY_NAME = "entryname"; 11 | 12 | @Override 13 | public String getConstraintName() { 14 | return CONSTRAINT_NAME; 15 | } 16 | 17 | @Override 18 | public String[] getFieldNames() { 19 | return new String[]{USER_NAME, ENTRY_NAME}; 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/model/constraints/UserNameUnique.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.model.constraints; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | @Component 6 | public class UserNameUnique implements DataUniqueConstraint { 7 | 8 | public static final String CONSTRAINT_NAME = "UNIQUE_USER_NAME"; 9 | public static final String FIELD_NAME = "name"; 10 | 11 | @Override 12 | public String getConstraintName() { 13 | return CONSTRAINT_NAME; 14 | } 15 | 16 | @Override 17 | public String[] getFieldNames() { 18 | return new String[]{FIELD_NAME}; 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/model/types/SessionStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.model.types; 2 | 3 | import com.github.maxamel.server.EnumUtils; 4 | import com.github.maxamel.server.IdentifierType; 5 | import java.util.Objects; 6 | 7 | /** 8 | * @author Max Amelchenko 9 | */ 10 | public enum SessionStatus implements IdentifierType { 11 | VALIDATED(1), WAITING(2), INVALIDATED(3), INITIATING(4); 12 | 13 | private final int id; 14 | 15 | SessionStatus(int id) { 16 | this.id = id; 17 | } 18 | 19 | public static SessionStatus byValue(int value) { 20 | if (Objects.nonNull(value)) { 21 | return EnumUtils.getByValue(SessionStatus.class, value); 22 | } 23 | 24 | return null; 25 | } 26 | 27 | @Override 28 | public Integer getValue() { 29 | return id; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/repositories/DiaryRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.repositories; 2 | 3 | import com.github.maxamel.server.domain.model.Diary; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.data.repository.RepositoryDefinition; 10 | 11 | /** 12 | * @author Max Amelchenko 13 | */ 14 | @RepositoryDefinition(domainClass = Diary.class, idClass = Long.class) 15 | public interface DiaryRepository { 16 | 17 | List findAll(); 18 | 19 | Page findAll(Pageable pageable); 20 | 21 | Optional findOne(long id); 22 | 23 | Optional findByUsernameAndEntryname(String username, String entryname); 24 | 25 | List findByUsername(String username); 26 | 27 | Diary save(Diary diary); 28 | 29 | void delete(long id); 30 | 31 | void deleteByUsernameAndEntryname(String username, String entryname); 32 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/domain/repositories/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain.repositories; 2 | 3 | import com.github.maxamel.server.domain.model.User; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.data.repository.RepositoryDefinition; 9 | 10 | /** 11 | * @author Max Amelchenko 12 | */ 13 | @RepositoryDefinition(domainClass = User.class, idClass = Long.class) 14 | public interface UserRepository { 15 | 16 | List findAll(); 17 | 18 | Page findAll(Pageable pageable); 19 | 20 | Optional findOne(long id); 21 | 22 | Optional findByName(String name); 23 | 24 | User save(User user); 25 | 26 | void delete(long id); 27 | 28 | void deleteByName(String name); 29 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/DiaryService.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services; 2 | 3 | import java.util.List; 4 | 5 | import com.github.maxamel.server.web.dtos.DiaryDto; 6 | 7 | /** 8 | * @author Max Amelchenko 9 | */ 10 | public interface DiaryService { 11 | 12 | public DiaryDto add(DiaryDto dto, String sessionId); 13 | 14 | public void removeByUsernameAndEntryname(String username, String entryname, String sessionId); 15 | 16 | public void removeAll(String username, String sessionId); 17 | 18 | public DiaryDto fetch(String username, String entryname, String sessionId); 19 | 20 | public List fetchByUsername(String username, String sessionId); 21 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/KafkaAgentService.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services; 2 | 3 | import com.github.maxamel.server.web.dtos.ChallengeDto; 4 | 5 | public interface KafkaAgentService { 6 | 7 | public void send(String topic, ChallengeDto chal); 8 | 9 | public void openTopic(String topic); 10 | 11 | public void closeTopic(String topic); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/ScheduleTaskService.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ScheduledExecutorService; 5 | 6 | import com.github.maxamel.server.domain.model.User; 7 | 8 | public interface ScheduleTaskService { 9 | 10 | public void publishChallenge(User user); 11 | 12 | public void handleActivity(User user, Map timers); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/UserService.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services; 2 | 3 | import org.springframework.dao.EmptyResultDataAccessException; 4 | import org.springframework.security.access.AccessDeniedException; 5 | 6 | import com.github.maxamel.server.domain.model.User; 7 | import com.github.maxamel.server.web.dtos.UserDto; 8 | 9 | /** 10 | * @author Max Amelchenko 11 | */ 12 | public interface UserService { 13 | 14 | public UserDto register(UserDto dto); 15 | 16 | public void removeByName(String name, String sessionId); 17 | 18 | public UserDto fetch(String name, String sessionId) throws AccessDeniedException, EmptyResultDataAccessException; 19 | 20 | public void generateServerSecret(User user); 21 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/impl/DiaryServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services.impl; 2 | 3 | import com.github.maxamel.server.web.dtos.DiaryDto; 4 | import com.github.maxamel.server.domain.model.Diary; 5 | import com.github.maxamel.server.domain.repositories.DiaryRepository; 6 | import com.github.maxamel.server.services.DiaryService; 7 | import com.github.maxamel.server.services.UserService; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | 13 | import org.modelmapper.ModelMapper; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.dao.EmptyResultDataAccessException; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.transaction.annotation.Propagation; 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | /** 21 | * @author Max Amelchenko 22 | */ 23 | @Service 24 | public class DiaryServiceImpl implements DiaryService { 25 | 26 | private final ModelMapper mapper; 27 | 28 | private final DiaryRepository repository; 29 | 30 | @Autowired 31 | private UserService userService; 32 | 33 | @Autowired 34 | public DiaryServiceImpl(ModelMapper mapper, DiaryRepository repository) { 35 | this.mapper = mapper; 36 | this.repository = repository; 37 | } 38 | 39 | @Override 40 | @Transactional 41 | public DiaryDto add(DiaryDto dto, String sessionId) { 42 | userService.fetch(dto.getUsername(), sessionId); 43 | Diary diary = mapper.map(dto, Diary.class); 44 | Diary newdiary = repository.save(diary); 45 | return mapper.map(newdiary, DiaryDto.class); 46 | } 47 | 48 | @Override 49 | @Transactional(propagation = Propagation.REQUIRES_NEW) 50 | public void removeByUsernameAndEntryname(String username, String entryname, String sessionId) { 51 | userService.fetch(username, sessionId); 52 | repository.deleteByUsernameAndEntryname(username, entryname); 53 | } 54 | 55 | @Override 56 | @Transactional 57 | public DiaryDto fetch(String username, String entryname, String sessionId) { 58 | userService.fetch(username, sessionId); 59 | Diary diary = repository.findByUsernameAndEntryname(username, entryname).orElseThrow(() -> new EmptyResultDataAccessException("No diary entry found for user: " + username + " and entry " + entryname, 1)); 60 | return mapper.map(diary, DiaryDto.class); 61 | } 62 | 63 | @Override 64 | @Transactional 65 | public List fetchByUsername(String username, String sessionId) { 66 | userService.fetch(username, sessionId); 67 | List entries = repository.findByUsername(username); 68 | return entries.stream().map(d -> mapper.map(d, DiaryDto.class)).collect(Collectors.toList()); 69 | } 70 | 71 | @Override 72 | @Transactional 73 | public void removeAll(String username, String sessionId) { 74 | userService.fetch(username, sessionId); 75 | List list = repository.findByUsername(username); 76 | for (Diary d : list) 77 | repository.deleteByUsernameAndEntryname(username, d.getEntryname()); 78 | } 79 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/impl/KafkaAgentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services.impl; 2 | 3 | import java.util.HashSet; 4 | import java.util.Properties; 5 | 6 | import org.I0Itec.zkclient.ZkClient; 7 | import org.I0Itec.zkclient.ZkConnection; 8 | import org.I0Itec.zkclient.exception.ZkMarshallingError; 9 | import org.I0Itec.zkclient.serialize.ZkSerializer; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 15 | import org.springframework.kafka.core.KafkaTemplate; 16 | import org.springframework.kafka.support.SendResult; 17 | import org.springframework.stereotype.Service; 18 | import org.springframework.util.concurrent.ListenableFuture; 19 | import org.springframework.util.concurrent.ListenableFutureCallback; 20 | 21 | import com.github.maxamel.server.services.KafkaAgentService; 22 | import com.github.maxamel.server.web.dtos.ChallengeDto; 23 | import com.github.rozidan.springboot.logger.Loggable; 24 | 25 | import kafka.admin.AdminUtils; 26 | import kafka.admin.RackAwareMode; 27 | import kafka.utils.ZKStringSerializer; 28 | import kafka.utils.ZkUtils; 29 | 30 | @Service 31 | @ConditionalOnProperty(value = "kafka.enabled", havingValue = "true") 32 | public class KafkaAgentServiceImpl implements KafkaAgentService{ 33 | 34 | private final KafkaTemplate kafkaTemplate; 35 | 36 | private final ZkUtils zkUtils; 37 | 38 | private final HashSet openTopics = new HashSet(); 39 | 40 | public KafkaAgentServiceImpl(@Value("${kafka.zookeeper.url}") String zkAddress, KafkaTemplate kafkaTemplate) 41 | { 42 | ZkClient zkClient = new ZkClient(zkAddress,10000,10000); 43 | zkClient.setZkSerializer(getZkSerializer()); 44 | boolean isSecureKafkaCluster = false; 45 | // ZkUtils for Kafka was used in Kafka 0.9.0.0 for the AdminUtils API 46 | this.zkUtils = new ZkUtils(zkClient, new ZkConnection(zkAddress), isSecureKafkaCluster); 47 | this.kafkaTemplate = kafkaTemplate; 48 | } 49 | 50 | @Loggable 51 | @Override 52 | public void send(String topic, ChallengeDto chal) { 53 | //make sure all messages with the same id will be ordered in the same partition 54 | ListenableFuture> future = kafkaTemplate.send(topic, chal); 55 | // register a callback with the listener to receive the result of the send 56 | // asynchronously 57 | future.addCallback(new ListenableFutureCallback>() { 58 | 59 | @Override 60 | public void onSuccess(SendResult result) { 61 | Logger log = LoggerFactory.getLogger(KafkaAgentServiceImpl.class); 62 | log.info("Published message to kafka..."); 63 | } 64 | 65 | @Override 66 | public void onFailure(Throwable ex) { 67 | Logger log = LoggerFactory.getLogger(KafkaAgentServiceImpl.class); 68 | log.info("Failed publishing message to kafka..." + ex.getMessage()); 69 | } 70 | }); 71 | 72 | } 73 | @Loggable 74 | @Override 75 | public void openTopic(String topic) 76 | { 77 | if (openTopics.contains(topic)) return; 78 | // Add topic configuration here 79 | if (!AdminUtils.topicExists(zkUtils, topic)) 80 | AdminUtils.createTopic(zkUtils, topic, 1, 1, new Properties(), RackAwareMode.Disabled$.MODULE$); 81 | 82 | openTopics.add(topic); 83 | } 84 | @Loggable 85 | @Override 86 | public void closeTopic(String topic) 87 | { 88 | // Add topic configuration here 89 | AdminUtils.deleteTopic(zkUtils, topic); 90 | openTopics.remove(topic); 91 | } 92 | 93 | private ZkSerializer getZkSerializer() { 94 | return new ZkSerializer() { 95 | @Override 96 | public byte[] serialize(Object o) 97 | throws ZkMarshallingError 98 | { 99 | return ZKStringSerializer.serialize(o); 100 | } 101 | 102 | @Override 103 | public Object deserialize(byte[] bytes) 104 | throws ZkMarshallingError 105 | { 106 | return ZKStringSerializer.deserialize(bytes); 107 | } 108 | }; 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/impl/ScheduleTaskServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services.impl; 2 | 3 | import java.math.BigInteger; 4 | import java.security.SecureRandom; 5 | import java.util.Map; 6 | import java.util.concurrent.ScheduledExecutorService; 7 | import java.util.concurrent.Semaphore; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.dao.EmptyResultDataAccessException; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | import com.github.maxamel.server.domain.model.User; 18 | import com.github.maxamel.server.domain.model.types.SessionStatus; 19 | import com.github.maxamel.server.domain.repositories.UserRepository; 20 | import com.github.maxamel.server.services.KafkaAgentService; 21 | import com.github.maxamel.server.services.ScheduleTaskService; 22 | import com.github.maxamel.server.web.dtos.ChallengeDto; 23 | 24 | @Service 25 | @Transactional(rollbackFor = InterruptedException.class) 26 | public class ScheduleTaskServiceImpl implements ScheduleTaskService{ 27 | 28 | @Autowired 29 | private UserRepository repository; 30 | 31 | @Autowired(required = false) 32 | private KafkaAgentService producer; 33 | 34 | @Value("${security.crypto.generator}") 35 | private String generator; 36 | 37 | @Value("${security.crypto.prime}") 38 | private String prime; 39 | 40 | private final Semaphore sem = new Semaphore(1); 41 | 42 | @Override 43 | public void publishChallenge(User olduser) 44 | { 45 | try 46 | { 47 | sem.acquire(); 48 | User user = repository.findByName(olduser.getName()).orElseThrow(() -> new EmptyResultDataAccessException("No user found with name: " + olduser.getName(), 1)); 49 | if (!user.getSstatus().equals(SessionStatus.INVALIDATED)) 50 | { 51 | producer.openTopic(user.getName()); 52 | SecureRandom random = new SecureRandom(); 53 | BigInteger bigint = new BigInteger(256, random); 54 | user.setSecret(bigint.toString(16)); 55 | repository.save(user); 56 | BigInteger power = new BigInteger(generator,16).modPow(new BigInteger(user.getSecret(),16), new BigInteger(prime,16)); 57 | ChallengeDto dto = new ChallengeDto(power.toString(16)); 58 | producer.send(olduser.getName(), dto); 59 | } 60 | sem.release(); 61 | } 62 | catch (InterruptedException e) 63 | { 64 | Logger log = LoggerFactory.getLogger(ScheduleTaskService.class); 65 | log.info("Publishing to Kafka interrupted!"); 66 | Thread.currentThread().interrupt(); 67 | } 68 | } 69 | 70 | @Override 71 | public void handleActivity(User olduser, Map kafkaTiming) 72 | { 73 | try 74 | { 75 | sem.acquire(); 76 | User user = repository.findByName(olduser.getName()).orElseThrow(() -> new EmptyResultDataAccessException("No user found with name: " + olduser.getName(), 1)); 77 | if (user.getSstatus().equals(SessionStatus.WAITING) || user.getSstatus().equals(SessionStatus.INITIATING)) 78 | { 79 | Logger log = LoggerFactory.getLogger(ScheduleTaskService.class); 80 | log.info("Inactivity threshold reached! Invalidating..." + user.getName()); 81 | user.setSstatus(SessionStatus.INVALIDATED); 82 | user.setSecret(null); 83 | if (producer != null) producer.closeTopic(user.getName()); 84 | repository.save(user); 85 | kafkaTiming.get(olduser.getId()).shutdown(); 86 | } 87 | else if (user.getSstatus().equals(SessionStatus.VALIDATED)) 88 | { 89 | Logger log = LoggerFactory.getLogger(ScheduleTaskService.class); 90 | log.info("Detected activity! Resuming..." + user.getName()); 91 | user.setSstatus(SessionStatus.WAITING); 92 | repository.save(user); 93 | } 94 | sem.release(); 95 | } 96 | catch (InterruptedException e) 97 | { 98 | Logger log = LoggerFactory.getLogger(ScheduleTaskService.class); 99 | log.info("Handling activity interrupted!"); 100 | Thread.currentThread().interrupt(); 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services.impl; 2 | 3 | import com.github.maxamel.server.web.dtos.UserDto; 4 | 5 | import com.github.maxamel.server.domain.model.User; 6 | import com.github.maxamel.server.domain.model.types.SessionStatus; 7 | import com.github.maxamel.server.domain.repositories.UserRepository; 8 | import com.github.maxamel.server.services.DiaryService; 9 | import com.github.maxamel.server.services.ScheduleTaskService; 10 | import com.github.maxamel.server.services.UserService; 11 | 12 | import java.math.BigInteger; 13 | import java.security.SecureRandom; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.concurrent.Executors; 17 | import java.util.concurrent.ScheduledExecutorService; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | import org.modelmapper.ModelMapper; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.beans.factory.annotation.Value; 23 | import org.springframework.dao.DataIntegrityViolationException; 24 | import org.springframework.dao.EmptyResultDataAccessException; 25 | import org.springframework.security.access.AccessDeniedException; 26 | import org.springframework.stereotype.Service; 27 | import org.springframework.transaction.annotation.Propagation; 28 | import org.springframework.transaction.annotation.Transactional; 29 | 30 | /** 31 | * @author Max Amelchenko 32 | */ 33 | @Service 34 | public class UserServiceImpl implements UserService { 35 | 36 | private final ModelMapper mapper; 37 | 38 | private final UserRepository repository; 39 | 40 | private final Map kafkaTiming = new HashMap<>(); 41 | 42 | @Value("${security.session.challengeFrequency}") 43 | private String chalFreq; 44 | 45 | @Value("${security.session.inactivityKickOut}") 46 | private String inactThreshold; 47 | 48 | @Value("${security.crypto.generator}") 49 | private String generator; 50 | 51 | @Value("${security.crypto.prime}") 52 | private String prime; 53 | 54 | @Value("${kafka.enabled}") 55 | private String kafka; 56 | 57 | @Autowired 58 | private ScheduleTaskService scheduler; 59 | 60 | @Autowired 61 | private DiaryService diaryService; 62 | 63 | @Autowired 64 | public UserServiceImpl(ModelMapper mapper, UserRepository repository) { 65 | this.mapper = mapper; 66 | this.repository = repository; 67 | } 68 | 69 | @Override 70 | @Transactional 71 | public UserDto register(UserDto dto) { 72 | User user = mapper.map(dto, User.class); 73 | repository.findByName(user.getName()).ifPresent(u -> { throw new DataIntegrityViolationException("User already exists with name: " + u.getName()); }); 74 | 75 | User newuser = repository.save(user); 76 | return mapper.map(newuser, UserDto.class); 77 | } 78 | 79 | @Override 80 | @Transactional 81 | public void removeByName(String name, String sessionId) { 82 | User user = repository.findByName(name).orElseThrow(() -> new EmptyResultDataAccessException("No user found with name: " + name, 1)); 83 | if (user.getSecret()!=null && verify(user,sessionId)) 84 | { 85 | diaryService.removeAll(name, sessionId); 86 | repository.deleteByName(name); 87 | } 88 | else 89 | { 90 | throwChallengedException(user); 91 | } 92 | } 93 | 94 | @Override 95 | @Transactional(propagation = Propagation.REQUIRES_NEW, noRollbackFor = {AccessDeniedException.class, EmptyResultDataAccessException.class}) 96 | public UserDto fetch(String name, String sessionId) throws AccessDeniedException, EmptyResultDataAccessException{ 97 | User user = repository.findByName(name).orElseThrow(() -> new EmptyResultDataAccessException("No user found with name: " + name, 1)); 98 | 99 | if (user.getSecret()!=null && verify(user,sessionId)) 100 | { 101 | return mapper.map(user, UserDto.class); 102 | } 103 | else 104 | { 105 | throwChallengedException(user); 106 | } 107 | return null; 108 | } 109 | 110 | private boolean verify(User user,String response) 111 | { 112 | BigInteger passwordless = new BigInteger(user.getPasswordless(),16); 113 | BigInteger secret = new BigInteger(user.getSecret(), 16); 114 | 115 | BigInteger verify = passwordless.modPow(secret, new BigInteger(prime,16)); 116 | 117 | if (equals(verify.toString(), response)) 118 | { 119 | SessionStatus status = user.getSstatus(); 120 | if (!user.getSstatus().equals(SessionStatus.VALIDATED)) 121 | { 122 | user.setSstatus(SessionStatus.VALIDATED); 123 | repository.save(user); 124 | } 125 | if (status.equals(SessionStatus.INITIATING)) scheduleAuthTask(user); 126 | } 127 | else 128 | { 129 | user.setSstatus(SessionStatus.INVALIDATED); 130 | user.setSecret(null); 131 | repository.save(user); 132 | if (kafkaTiming.containsKey(user.getId())) 133 | { 134 | ScheduledExecutorService execService = kafkaTiming.get(user.getId()); 135 | execService.shutdownNow(); 136 | } 137 | return false; 138 | } 139 | return true; 140 | } 141 | 142 | // perform constant time string comparison against timing attacks 143 | private boolean equals(String a, String b) 144 | { 145 | if (a.length() != b.length()) return false; 146 | boolean bool = true; 147 | for (int i=0; i new EmptyResultDataAccessException("No user found with name: " + olduser.getName(), 1)); 164 | SecureRandom random = new SecureRandom(); 165 | BigInteger bigint = new BigInteger(256, random); 166 | user.setSecret(bigint.toString(16)); 167 | user.setSstatus(SessionStatus.INITIATING); 168 | repository.save(user); 169 | } 170 | 171 | private void scheduleAuthTask(User user) 172 | { 173 | ScheduledExecutorService execService = Executors.newScheduledThreadPool(2); 174 | if ("true".equals(kafka)) 175 | { 176 | Runnable r1 = () -> {scheduler.publishChallenge(user);}; 177 | execService.scheduleAtFixedRate(r1, 0, Long.parseLong(chalFreq), TimeUnit.MILLISECONDS); 178 | } 179 | 180 | Runnable r2 = () -> {scheduler.handleActivity(user, kafkaTiming);}; 181 | execService.scheduleAtFixedRate(r2, Long.parseLong(inactThreshold), Long.parseLong(inactThreshold), TimeUnit.MILLISECONDS); 182 | 183 | kafkaTiming.put(user.getId(), execService); 184 | } 185 | 186 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/services/mapping/MapperConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services.mapping; 2 | 3 | import com.github.rozidan.springboot.modelmapper.ConfigurationConfigurer; 4 | import org.modelmapper.config.Configuration; 5 | import org.modelmapper.convention.MatchingStrategies; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class MapperConfiguration extends ConfigurationConfigurer { 10 | @Override 11 | public void configure(Configuration configuration) { 12 | configuration.setMatchingStrategy(MatchingStrategies.STRICT); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/controllers/DiaryController.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.controllers; 2 | 3 | import com.github.maxamel.server.web.dtos.DiaryDto; 4 | import com.github.maxamel.server.web.dtos.errors.ErrorDto; 5 | import com.github.maxamel.server.services.DiaryService; 6 | import com.github.rozidan.springboot.logger.Loggable; 7 | import io.swagger.annotations.Api; 8 | import io.swagger.annotations.ApiOperation; 9 | import io.swagger.annotations.ApiResponse; 10 | import io.swagger.annotations.ApiResponses; 11 | 12 | import java.util.List; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.validation.annotation.Validated; 17 | import org.springframework.web.bind.annotation.CrossOrigin; 18 | import org.springframework.web.bind.annotation.DeleteMapping; 19 | import org.springframework.web.bind.annotation.GetMapping; 20 | import org.springframework.web.bind.annotation.PathVariable; 21 | import org.springframework.web.bind.annotation.PostMapping; 22 | import org.springframework.web.bind.annotation.RequestBody; 23 | import org.springframework.web.bind.annotation.RequestHeader; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.ResponseStatus; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | /** 29 | * @author Max Amelchenko 30 | */ 31 | @Loggable(ignore = Exception.class) 32 | @Api(tags = "Diaries") 33 | @CrossOrigin(origins = "*", allowedHeaders = "*") 34 | @RestController 35 | @RequestMapping(path = "/diary") 36 | public class DiaryController { 37 | 38 | private final DiaryService diaryService; 39 | 40 | @Autowired 41 | public DiaryController(DiaryService diaryService) { 42 | this.diaryService = diaryService; 43 | } 44 | 45 | @ApiOperation(value = "Add new entry") 46 | @ApiResponses({ 47 | @ApiResponse(code = 201, message = "Successfully created entry"), 48 | @ApiResponse(code = 428, message = "Invalid user info", response = ErrorDto.class)}) 49 | @ResponseStatus(code = HttpStatus.CREATED) 50 | @PostMapping 51 | public DiaryDto add(@Validated @RequestBody DiaryDto dto, @RequestHeader(value="ZKAuth-Token", required=false) String token) { 52 | return diaryService.add(dto, token); 53 | } 54 | 55 | @ApiOperation("Delete entry") 56 | @ApiResponses({ 57 | @ApiResponse(code = 200, message = "Entry has been removed"), 58 | @ApiResponse(code = 401, message = "Unauthorized Access"), 59 | @ApiResponse(code = 404, message = "Entry not found")}) 60 | @ResponseStatus(HttpStatus.OK) 61 | @DeleteMapping("/{username}/{entryname}") 62 | public void remove(@PathVariable(required=true) String username, @PathVariable(required=true) String entryname, @RequestHeader(value="ZKAuth-Token", required=false) String token) { 63 | diaryService.removeByUsernameAndEntryname(username, entryname, token); 64 | } 65 | 66 | @ApiOperation("Retrieving existing user") 67 | @ApiResponses({ 68 | @ApiResponse(code = 200, message = "Successfully fetched entry list"), 69 | @ApiResponse(code = 401, message = "Unauthorized Access"), 70 | @ApiResponse(code = 404, message = "No entries Found")}) 71 | @ResponseStatus(HttpStatus.OK) 72 | @GetMapping("/{username}") 73 | public List fetch(@PathVariable String username, @RequestHeader(value="ZKAuth-Token", required=false) String token) { 74 | return diaryService.fetchByUsername(username, token); 75 | } 76 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/controllers/UserController.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.controllers; 2 | 3 | import com.github.maxamel.server.web.dtos.UserDto; 4 | import com.github.maxamel.server.web.dtos.errors.ErrorDto; 5 | import com.github.maxamel.server.services.UserService; 6 | import com.github.rozidan.springboot.logger.Loggable; 7 | import io.swagger.annotations.Api; 8 | import io.swagger.annotations.ApiOperation; 9 | import io.swagger.annotations.ApiResponse; 10 | import io.swagger.annotations.ApiResponses; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.validation.annotation.Validated; 15 | import org.springframework.web.bind.annotation.CrossOrigin; 16 | import org.springframework.web.bind.annotation.DeleteMapping; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.PostMapping; 20 | import org.springframework.web.bind.annotation.RequestBody; 21 | import org.springframework.web.bind.annotation.RequestHeader; 22 | import org.springframework.web.bind.annotation.RequestMapping; 23 | import org.springframework.web.bind.annotation.ResponseStatus; 24 | import org.springframework.web.bind.annotation.RestController; 25 | 26 | /** 27 | * @author Max Amelchenko 28 | */ 29 | @Loggable(ignore = Exception.class) 30 | @Api(tags = "Users") 31 | @RestController 32 | @CrossOrigin(origins = "*", allowedHeaders = "*") 33 | @RequestMapping(path = "/users") 34 | public class UserController { 35 | 36 | private final UserService userService; 37 | 38 | @Autowired 39 | public UserController(UserService userService) { 40 | this.userService = userService; 41 | } 42 | 43 | @ApiOperation(value = "Register new user") 44 | @ApiResponses({ 45 | @ApiResponse(code = 201, message = "Successfully registered user"), 46 | @ApiResponse(code = 409, message = "User already exists"), 47 | @ApiResponse(code = 428, message = "Invalid user info", response = ErrorDto.class)}) 48 | @ResponseStatus(code = HttpStatus.CREATED) 49 | @PostMapping 50 | public UserDto register(@Validated @RequestBody UserDto dto) { 51 | return userService.register(dto); 52 | } 53 | 54 | @ApiOperation("Delete user") 55 | @ApiResponses({ 56 | @ApiResponse(code = 200, message = "User has been removed"), 57 | @ApiResponse(code = 401, message = "Unauthorized Access"), 58 | @ApiResponse(code = 404, message = "User not found")}) 59 | @ResponseStatus(HttpStatus.OK) 60 | @DeleteMapping("/{name}") 61 | public void remove(@PathVariable String name, @RequestHeader(value="ZKAuth-Token", required=false) String token) { 62 | userService.removeByName(name, token); 63 | } 64 | 65 | @ApiOperation("Retrieving existing user") 66 | @ApiResponses({ 67 | @ApiResponse(code = 200, message = "Successfully fetched user"), 68 | @ApiResponse(code = 401, message = "Unauthorized Access"), 69 | @ApiResponse(code = 404, message = "User not Found")}) 70 | @ResponseStatus(HttpStatus.OK) 71 | @GetMapping("/{name}") 72 | public UserDto fetch(@PathVariable String name, @RequestHeader(value="ZKAuth-Token", required=false) String token) { 73 | return userService.fetch(name, token); 74 | } 75 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/ChallengeDto.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos; 2 | 3 | import com.github.maxamel.server.web.dtos.audit.AuditableDto; 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | 12 | /** 13 | * @author Max Amelchenko 14 | */ 15 | @ApiModel("Challenge") 16 | @Getter 17 | @AllArgsConstructor 18 | @Builder 19 | public class ChallengeDto extends AuditableDto { 20 | private static final long serialVersionUID = 5762617605382814204L; 21 | 22 | @ApiModelProperty(required = true) 23 | @NotNull 24 | private String challenge; 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/DiaryDto.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos; 2 | 3 | import com.github.maxamel.server.web.dtos.audit.AuditableDto; 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.Setter; 12 | import org.hibernate.validator.constraints.NotEmpty; 13 | 14 | /** 15 | * @author Max Amelchenko 16 | */ 17 | @ApiModel("Diary") 18 | @Getter 19 | @Setter 20 | @AllArgsConstructor 21 | @NoArgsConstructor 22 | @Builder 23 | public class DiaryDto extends AuditableDto { 24 | private static final long serialVersionUID = 5762617605382814204L; 25 | 26 | @ApiModelProperty(required = true) 27 | private Long id; 28 | 29 | @ApiModelProperty(required = true) 30 | @NotEmpty 31 | private String username; 32 | 33 | @ApiModelProperty(required = true) 34 | @NotEmpty 35 | private String entryname; 36 | 37 | @ApiModelProperty(required = true) 38 | @NotEmpty 39 | private String content; 40 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/ErrorCodes.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos; 2 | 3 | /** 4 | * @author Idan Rozenfeld 5 | */ 6 | public enum ErrorCodes { 7 | UNAUTHORIZED, 8 | FORBIDDEN, 9 | NOT_FOUND, 10 | REQUEST_NOT_READABLE, 11 | UNKNOWN, 12 | MISSING_REQUEST_PARAM, 13 | DATA_VALIDATION, 14 | METHOD_NOT_ALLOWED, 15 | HTTP_MEDIA_TYPE_NOT_SUPPORTED 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/UserDto.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.github.maxamel.server.domain.model.types.SessionStatus; 5 | import com.github.maxamel.server.web.dtos.audit.AuditableDto; 6 | import io.swagger.annotations.ApiModel; 7 | import io.swagger.annotations.ApiModelProperty; 8 | 9 | import javax.validation.constraints.NotNull; 10 | import lombok.AllArgsConstructor; 11 | import lombok.Builder; 12 | import lombok.Getter; 13 | import lombok.NoArgsConstructor; 14 | import lombok.Setter; 15 | import org.hibernate.validator.constraints.NotEmpty; 16 | 17 | /** 18 | * @author Max Amelchenko 19 | */ 20 | @ApiModel("User") 21 | @Getter 22 | @Setter 23 | @AllArgsConstructor 24 | @NoArgsConstructor 25 | @Builder 26 | public class UserDto extends AuditableDto { 27 | private static final long serialVersionUID = 5762617605382814204L; 28 | 29 | @ApiModelProperty(required = true) 30 | private Long id; 31 | 32 | @ApiModelProperty(required = true) 33 | @NotEmpty 34 | private String name; 35 | 36 | @ApiModelProperty(required = true) 37 | @NotNull 38 | private String passwordless; 39 | 40 | @ApiModelProperty(required = false) 41 | @JsonIgnore 42 | private String secret; 43 | 44 | @ApiModelProperty(required = false) 45 | @JsonIgnore 46 | private SessionStatus sstatus; 47 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/audit/AuditableDto.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos.audit; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import java.io.Serializable; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | @ApiModel("Audit") 10 | public class AuditableDto implements Serializable { 11 | private static final long serialVersionUID = -159284889641683544L; 12 | 13 | @ApiModelProperty(readOnly = true) 14 | private String createdBy; 15 | 16 | @ApiModelProperty(readOnly = true) 17 | private String lastModifiedBy; 18 | 19 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/errors/ErrorDto.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos.errors; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import java.io.Serializable; 5 | import java.util.Set; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.Singular; 9 | 10 | 11 | @ApiModel("Error") 12 | @Getter 13 | @Builder 14 | public class ErrorDto implements Serializable { 15 | private static final long serialVersionUID = -4708936233513887899L; 16 | 17 | private Enum errorCode; 18 | 19 | private String message; 20 | 21 | private String challenge; 22 | 23 | @Singular 24 | private Set errors; 25 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/errors/HttpMediaTypeErrorDto.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos.errors; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import java.io.Serializable; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | @ApiModel("HttpMediaTypeError") 10 | @Getter 11 | @Setter 12 | @Builder 13 | public class HttpMediaTypeErrorDto implements Serializable { 14 | private static final long serialVersionUID = 7301072886218818L; 15 | 16 | private String mediaType; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/errors/HttpRequestMethodErrorDto.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos.errors; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import java.io.Serializable; 5 | import java.util.List; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.Singular; 9 | import org.springframework.http.HttpMethod; 10 | 11 | @ApiModel("HttpRequestMethodError") 12 | @Getter 13 | @Builder 14 | public class HttpRequestMethodErrorDto implements Serializable { 15 | private static final long serialVersionUID = 4115067500106084449L; 16 | 17 | private String actualMethod; 18 | 19 | @Singular 20 | private List supportedMethods; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/dtos/errors/ValidationErrorDto.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.dtos.errors; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import java.io.Serializable; 5 | import java.util.List; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.Singular; 9 | 10 | @ApiModel("ValidationError") 11 | @Getter 12 | @Builder 13 | public class ValidationErrorDto implements Serializable { 14 | private static final long serialVersionUID = 6692364309366067411L; 15 | 16 | private String fieldName; 17 | 18 | private String errorCode; 19 | 20 | private Object rejectedValue; 21 | 22 | @Singular 23 | private List params; 24 | 25 | private String message; 26 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/handlers/DataExceptionHandlers.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.handlers; 2 | 3 | import com.github.maxamel.server.web.dtos.ErrorCodes; 4 | import com.github.maxamel.server.web.dtos.errors.ErrorDto; 5 | import com.github.maxamel.server.web.dtos.errors.ValidationErrorDto; 6 | import com.github.maxamel.server.domain.model.constraints.DataUniqueConstraint; 7 | import com.github.rozidan.springboot.logger.Loggable; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.stream.Collectors; 12 | import org.hibernate.exception.ConstraintViolationException; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.core.Ordered; 15 | import org.springframework.core.annotation.Order; 16 | import org.springframework.dao.DataIntegrityViolationException; 17 | import org.springframework.dao.EmptyResultDataAccessException; 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.web.bind.annotation.ExceptionHandler; 20 | import org.springframework.web.bind.annotation.ResponseStatus; 21 | import org.springframework.web.bind.annotation.RestControllerAdvice; 22 | 23 | @Order(Ordered.HIGHEST_PRECEDENCE) 24 | @RestControllerAdvice 25 | public class DataExceptionHandlers { 26 | 27 | private final List uniqueConstraintList; 28 | 29 | @Autowired 30 | public DataExceptionHandlers(List uniqueConstraintList) { 31 | this.uniqueConstraintList = uniqueConstraintList; 32 | } 33 | 34 | @Loggable 35 | @ResponseStatus(code = HttpStatus.NOT_FOUND) 36 | @ExceptionHandler(EmptyResultDataAccessException.class) 37 | public ErrorDto handleEmptyResultDataAccessException(EmptyResultDataAccessException ex) { 38 | return ErrorDto.builder() 39 | .errorCode(ErrorCodes.NOT_FOUND) 40 | .message(ex.getLocalizedMessage()) 41 | .build(); 42 | } 43 | 44 | @Loggable 45 | @ResponseStatus(code = HttpStatus.CONFLICT) 46 | @ExceptionHandler(DataIntegrityViolationException.class) 47 | public ErrorDto handleDataIntegrityViolationException(DataIntegrityViolationException ex) { 48 | if (uniqueConstraintList != null && ex.getCause() != null && ex.getCause() instanceof ConstraintViolationException) { 49 | Optional matchCons = uniqueConstraintList.stream().filter((cons) -> 50 | ((ConstraintViolationException) ex.getCause()).getConstraintName().contains(cons.getConstraintName())).findFirst(); 51 | if (matchCons.isPresent()) { 52 | return ErrorDto.builder() 53 | .errorCode(ErrorCodes.DATA_VALIDATION) 54 | .error(ValidationErrorDto.builder() 55 | .errorCode("UNIQUE") 56 | .fieldName(Arrays.stream(matchCons.get().getFieldNames()) 57 | .map(Object::toString) 58 | .collect(Collectors.joining(", "))) 59 | .build()) 60 | .message(ex.getMessage()) 61 | .build(); 62 | } 63 | } 64 | return ErrorDto.builder() 65 | .errorCode(ErrorCodes.UNKNOWN) 66 | .message(ex.getLocalizedMessage()) 67 | .build(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/handlers/GlobalErrorHandlers.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.handlers; 2 | 3 | import com.github.maxamel.server.web.dtos.ErrorCodes; 4 | import com.github.maxamel.server.web.dtos.errors.ErrorDto; 5 | import com.github.maxamel.server.web.dtos.errors.HttpMediaTypeErrorDto; 6 | import com.github.maxamel.server.web.dtos.errors.HttpRequestMethodErrorDto; 7 | import com.github.rozidan.springboot.logger.Loggable; 8 | import java.util.Collections; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.boot.logging.LogLevel; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.web.HttpMediaTypeNotSupportedException; 13 | import org.springframework.web.HttpRequestMethodNotSupportedException; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | import org.springframework.web.bind.annotation.ResponseStatus; 16 | import org.springframework.web.bind.annotation.RestControllerAdvice; 17 | import org.springframework.web.servlet.NoHandlerFoundException; 18 | 19 | /** 20 | * @author Idan Rozenfeld 21 | */ 22 | @Slf4j 23 | @RestControllerAdvice 24 | public class GlobalErrorHandlers { 25 | 26 | @ResponseStatus(code = HttpStatus.NOT_FOUND) 27 | @ExceptionHandler(NoHandlerFoundException.class) 28 | public ErrorDto handleNoHandlerFoundException(NoHandlerFoundException ex) { 29 | return ErrorDto.builder() 30 | .errorCode(ErrorCodes.NOT_FOUND) 31 | .message(ex.getLocalizedMessage()) 32 | .build(); 33 | } 34 | 35 | @Loggable 36 | @ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED) 37 | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 38 | public ErrorDto handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex) { 39 | return ErrorDto.builder() 40 | .errorCode(ErrorCodes.METHOD_NOT_ALLOWED) 41 | .errors(Collections.singleton(HttpRequestMethodErrorDto.builder() 42 | .actualMethod(ex.getMethod()) 43 | .supportedMethods(ex.getSupportedHttpMethods()) 44 | .build())) 45 | .message(ex.getLocalizedMessage()) 46 | .build(); 47 | } 48 | 49 | @Loggable 50 | @ResponseStatus(code = HttpStatus.UNSUPPORTED_MEDIA_TYPE) 51 | @ExceptionHandler(HttpMediaTypeNotSupportedException.class) 52 | public ErrorDto handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException ex) { 53 | return ErrorDto.builder() 54 | .errorCode(ErrorCodes.HTTP_MEDIA_TYPE_NOT_SUPPORTED) 55 | .errors(Collections.singleton(HttpMediaTypeErrorDto.builder() 56 | .mediaType(ex.getContentType().toString()) 57 | .build())) 58 | .message(ex.getLocalizedMessage()) 59 | .build(); 60 | } 61 | 62 | @Loggable(LogLevel.ERROR) 63 | @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) 64 | @ExceptionHandler(Exception.class) 65 | public ErrorDto handleGlobalError(Exception ex) { 66 | log.error("Global error handler exception: ", ex); 67 | return ErrorDto.builder() 68 | .errorCode(ErrorCodes.UNKNOWN) 69 | .message(ex.getLocalizedMessage()) 70 | .build(); 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/com/github/maxamel/server/web/handlers/ValidationErrorHandlers.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web.handlers; 2 | 3 | import com.github.maxamel.server.web.dtos.ErrorCodes; 4 | import com.github.maxamel.server.web.dtos.errors.ErrorDto; 5 | import com.github.rozidan.springboot.logger.Loggable; 6 | 7 | import org.springframework.core.Ordered; 8 | import org.springframework.core.annotation.Order; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.converter.HttpMessageNotReadableException; 11 | import org.springframework.security.access.AccessDeniedException; 12 | import org.springframework.web.bind.annotation.ExceptionHandler; 13 | import org.springframework.web.bind.annotation.ResponseStatus; 14 | import org.springframework.web.bind.annotation.RestControllerAdvice; 15 | 16 | /** 17 | * @author Max Amelchenko 18 | */ 19 | @Order(Ordered.HIGHEST_PRECEDENCE) 20 | @RestControllerAdvice 21 | public class ValidationErrorHandlers { 22 | 23 | @Loggable 24 | @ResponseStatus(code = HttpStatus.BAD_REQUEST) 25 | @ExceptionHandler(HttpMessageNotReadableException.class) 26 | public ErrorDto handleNotReadableError(HttpMessageNotReadableException ex) { 27 | return ErrorDto.builder() 28 | .errorCode(ErrorCodes.REQUEST_NOT_READABLE) 29 | .message(ex.getLocalizedMessage()) 30 | .build(); 31 | } 32 | 33 | @Loggable 34 | @ResponseStatus(code = HttpStatus.UNAUTHORIZED) 35 | @ExceptionHandler(AccessDeniedException.class) 36 | public ErrorDto handleUnauthorizedRequest(AccessDeniedException ex) { 37 | return ErrorDto.builder() 38 | .errorCode(ErrorCodes.UNAUTHORIZED) 39 | .challenge(ex.getMessage()) 40 | .message("Unauthorized") 41 | .build(); 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring-devtools.properties: -------------------------------------------------------------------------------- 1 | restart.include.modelmapper=/modelmapper-.*\.jar -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | 3 | datasource: 4 | url: jdbc:h2:~/test 5 | username: sa 6 | password: 7 | 8 | jpa: 9 | show-sql: false 10 | 11 | hibernate: 12 | ddl-auto: create-drop 13 | transaction: 14 | flush-before-completion: true 15 | use-new-id-generator-mappings: true 16 | naming: 17 | implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl 18 | 19 | mvc: 20 | throw-exception-if-no-handler-found: true 21 | 22 | jackson: 23 | default-property-inclusion: non_null 24 | serialization: 25 | write-dates-as-timestamps: false 26 | 27 | resources: 28 | add-mappings: false 29 | 30 | h2: 31 | console: 32 | enabled: true 33 | 34 | server: 35 | context-path: /zkauth 36 | port: 8080 37 | 38 | security: 39 | basic: 40 | enabled: false 41 | session: 42 | inactivityKickOut: 360000 43 | challengeFrequency: 12000 44 | crypto: 45 | generator: AC4032EF4F2D9AE39DF30B5C8FFDAC506CDEBE7B89998CAF74866A08CFE4FFE3A6824A4E10B9A6F0DD921F01A70C4AFAAB739D7700C29F52C57DB17C620A8652BE5E9001A8D66AD7C17669101999024AF4D027275AC1348BB8A762D0521BC98AE247150422EA1ED409939D54DA7460CDB5F6C6B250717CBEF180EB34118E98D119529A45D6F834566E3025E316A330EFBB77A86F0C1AB15B051AE3D428C8F8ACB70A8137150B8EEB10E183EDD19963DDD9E263E4770589EF6AA21E7F5F2FF381B539CCE3409D13CD566AFBB48D6C019181E1BCFE94B30269EDFE72FE9B6AA4BD7B5A0F1C71CFFF4C19C418E1F6EC017981BC087F2A7065B384B890D3191F2BFA 46 | prime: AD107E1E9123A9D0D660FAA79559C51FA20D64E5683B9FD1B54B1597B61D0A75E6FA141DF95A56DBAF9A3C407BA1DF15EB3D688A309C180E1DE6B85A1274A0A66D3F8152AD6AC2129037C9EDEFDA4DF8D91E8FEF55B7394B7AD5B7D0B6C12207C9F98D11ED34DBF6C6BA0B2C8BBC27BE6A00E0A0B9C49708B3BF8A317091883681286130BC8985DB1602E714415D9330278273C7DE31EFDC7310F7121FD5A07415987D9ADC0A486DCDF93ACC44328387315D75E198C641A480CD86A1B9E587E8BE60E69CC928B2B9C52172E413042E9B23F10B0E16E79763C9B53DCF4BA80A29E3FB73C16B8E75B97EF363E2FFA31F71CF9DE5384E71B81C0AC4DFFE0C10E64F 47 | 48 | file: logs/zkauth.log 49 | level: 50 | root: trace 51 | com.github.maxamel: trace 52 | 53 | flyway: 54 | enabled: false 55 | 56 | management: 57 | security.enabled: false 58 | 59 | kafka: 60 | enabled: true 61 | zookeeper: 62 | url: "10.8.120.53:2181" 63 | broker: 64 | url: "10.8.120.53:9092" 65 | 66 | test: 67 | username: "John" 68 | entryname: "MyDiary" 69 | passwordless: "a3166e7b31408c5a5c002d86b26069b69dbb91683b180c1a83533b5ea3e7c612744174a5fb9dc604e3b5c5310de40830e9cf6a61fdc71c2732cd49f18728c7961ea6e054c9c5285b8c7d4d79d3ef1fde61172dc03e69a9daf77dc77de71bc11aa05cc15f4cd007fdc72bc5bb5b4475f0d4e0c84ed059692ab4766fa1400adae5bdc3f3b9dcc0ab5af8e00924df40ed9878ed11818059005f31551323a12eece1e0ecb031ff1201093cc632ec23951420298fd4f07f560fd270eaa2ba4cf688a7f2c444a72caee47e35edd42faefabc7f596c150cc6a826eac77d94e1c0d82a28364260c678831178c8d85302f0f6d1be1ec5f99a2ac61d47a6072bb415635606" 70 | secret: "68566D5971337436763979244226452948404D635166546A576E5A7234753778" 71 | wrongsecret: "68566D5971337436763979244226452948404D635166546A576E5A7234753771" 72 | answer: "1357118099208700368057074407633646596383265634565817441322774393623597414394421842139088174540908661449333787919910206213141809109604625571630599765212661891148085701263683900613944161466356908696445925362994350641580306372340499894972171648878159229715824673546138156092456135816881867757428587148715197849470096718241278293154865951091942868588343340591942158166002485377540188238462054111356889129468076311709323377114534883018407569143379681131134166943870301882342218168464399099937038878889630569344125000704940858829283581403901919844100622216626934541017662416314902374797221348317924607266677347117504727200" -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${Ansi.BLUE} 2 | _________ .__ ______________ __. _____ __ .__ 3 | / _____/____________|__| ____ ____\____ / |/ _| / _ \ __ ___/ |_| |__ 4 | \_____ \\____ \_ __ \ |/ \ / ___\ / /| < / /_\ \| | \ __\ | \ 5 | / \ |_> > | \/ | | \/ /_/ > /_| | \/ | \ | /| | | Y \ 6 | /_______ / __/|__| |__|___| /\___ /_______ \____|__ \____|__ /____/ |__| |___| / 7 | \/|__| \//_____/ \/ \/ \/ \/ 8 | ::${application.title}:: ${application.formatted-version} 9 | ${Ansi.DEFAULT} -------------------------------------------------------------------------------- /src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application.name: ZKAUTH -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS user ( 2 | `id` BIGINT 3 | created_by VARCHAR(255), 4 | last_modified_by VARCHAR(255), 5 | `name` VARCHAR(255) NOT NULL, 6 | `passwordless` VARCHAR(600), NOT NULL, 7 | `secret` VARCHAR(600), 8 | `sstatus` VARCHAR(255), 9 | PRIMARY KEY (`id`), 10 | UNIQUE KEY `UNIQUE_USER_NAME` (`name`) 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS user_seq ( 14 | next_val BIGINT(20) NULL DEFAULT NULL 15 | ); 16 | 17 | CREATE TABLE IF NOT EXISTS diary ( 18 | `id` BIGINT 19 | created_by VARCHAR(255), 20 | last_modified_by VARCHAR(255), 21 | `username` VARCHAR(255) NOT NULL, 22 | `entryname` VARCHAR(255) NOT NULL, 23 | `content` VARCHAR(600) NOT NULL, 24 | PRIMARY KEY (`id`), 25 | UNIQUE KEY `UNIQUE_USERENTRY_NAME` (`username`,`entryname`) 26 | ); 27 | 28 | CREATE TABLE IF NOT EXISTS diary_seq ( 29 | next_val BIGINT(20) NULL DEFAULT NULL 30 | ); 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam backgroundColor #EEEBDC 3 | skinparam handwritten true 4 | skinparam sequence { 5 | ArrowColor DeepSkyBlue 6 | ActorBorderColor DeepSkyBlue 7 | EntityBoderColor DeepSkyBlue 8 | LifeLineBorderColor blue 9 | LifeLineBackgroundColor #A9DCDF 10 | 11 | EntityBorderColor DeepSkyBlue 12 | EntityBackgroundColor DodgerBlue 13 | EntityFontName Impact 14 | EntityFontSize 17 15 | EntityFontColor #A9DCDF 16 | 17 | ActorBackgroundColor aqua 18 | ActorFontColor DeepSkyBlue 19 | ActorFontSize 17 20 | ActorFontName Aapex 21 | 22 | DatabaseBackgroundColor aqua 23 | DatabaseFontColor DeepSkyBlue 24 | DatabaseFontSize 17 25 | DatabaseFontName Aapex 26 | } 27 | legend top center 28 | Public variables: 29 | Large generator g 30 | Large prime N 31 | end legend 32 | 33 | group Register 34 | actor Client 35 | entity Server 36 | Client ->o Client : x 37 | Client -> Server : g^x mod N 38 | database DB 39 | Server -> DB : g^x mod N 40 | Server -[#green]> Client : 201 CREATED 41 | end 42 | group Request 43 | Client -> Server : GET /something 44 | Server ->o Server : y 45 | Server -[#red]> Client : 401 Unauthorized 46 | note left: Challenge:g^y mod N 47 | Client -> Server : GET /something 48 | note right : sessionID:(g^y)^x mod N 49 | DB -> Server : g^x mod N 50 | Server ->o Server : (g^x)^y mod N 51 | Server -[#green]> Client : 200 OK 52 | Client -> Server : GET /somethingelse 53 | note right : sessionID:(g^y)^x mod N 54 | Server -[#green]> Client : 200 OK 55 | end 56 | @enduml -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/config/JsonConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.ObjectWriter; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 6 | import org.springframework.boot.test.context.TestConfiguration; 7 | import org.springframework.context.annotation.Bean; 8 | 9 | @TestConfiguration 10 | public class JsonConfiguration { 11 | @Bean 12 | @ConditionalOnMissingBean(ObjectWriter.class) 13 | public ObjectWriter testObjectWriter() { 14 | ObjectMapper mapper = new ObjectMapper(); 15 | return mapper.writer().withDefaultPrettyPrinter(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/domain/DiaryRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain; 2 | 3 | import static org.hamcrest.Matchers.equalTo; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.junit.Assert.assertThat; 6 | import static org.junit.Assert.assertTrue; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 16 | import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | 19 | import com.github.maxamel.server.domain.model.Diary; 20 | import com.github.maxamel.server.domain.repositories.DiaryRepository; 21 | 22 | @RunWith(SpringRunner.class) 23 | @DataJpaTest 24 | public class DiaryRepositoryTest { 25 | 26 | @Autowired 27 | private TestEntityManager entityManager; 28 | 29 | @Autowired 30 | private DiaryRepository repository; 31 | 32 | @Value("{test.username}") 33 | private String username; 34 | 35 | @Value("${test.entryname}") 36 | private String entryname; 37 | 38 | @Value("${test.pass}") 39 | private String content; 40 | 41 | @Test 42 | public void findOneShouldSuccessTest() { 43 | Diary persist = entityManager.persist(Diary.builder() 44 | .username(username) 45 | .entryname(entryname) 46 | .content(content) 47 | .build()); 48 | 49 | Optional diary = repository.findOne(persist.getId()); 50 | assertTrue(diary.isPresent()); 51 | assertThat(diary.get().getUsername(), is(equalTo(username))); 52 | assertThat(diary.get().getEntryname(), is(equalTo(entryname))); 53 | assertThat(diary.get().getContent(), is(equalTo(content))); 54 | } 55 | 56 | @Test 57 | public void findByUsernameShouldSuccessTest() { 58 | Diary persist1 = entityManager.persist(Diary.builder() 59 | .username(username) 60 | .entryname(entryname) 61 | .content(content) 62 | .build()); 63 | 64 | Diary persist2 = entityManager.persist(Diary.builder() 65 | .username(username) 66 | .entryname(entryname+"1") 67 | .content(content) 68 | .build()); 69 | 70 | List list = repository.findByUsername(username); 71 | assertTrue(list.size() == 2); 72 | assertTrue(list.get(0).getId().equals(persist1.getId())); 73 | assertTrue(list.get(1).getId().equals(persist2.getId())); 74 | } 75 | 76 | @Test 77 | public void removeShouldSuccessTest() { 78 | Diary persist = entityManager.persist(Diary.builder() 79 | .username(username) 80 | .entryname(entryname) 81 | .content(content) 82 | .build()); 83 | 84 | repository.delete(persist.getId()); 85 | Optional diary = repository.findOne(persist.getId()); 86 | assertTrue(!diary.isPresent()); 87 | 88 | } 89 | 90 | @Test 91 | public void removeByUsernameAndEntrynameShouldSuccessTest() { 92 | Diary persist = entityManager.persist(Diary.builder() 93 | .username(username) 94 | .entryname(entryname) 95 | .content(content) 96 | .build()); 97 | 98 | repository.deleteByUsernameAndEntryname(persist.getUsername(), persist.getEntryname()); 99 | Optional diary = repository.findOne(persist.getId()); 100 | assertTrue(!diary.isPresent()); 101 | } 102 | 103 | @Test 104 | public void findAllShouldSuccessTest() { 105 | Diary persist1 = entityManager.persist(Diary.builder() 106 | .username(username) 107 | .entryname(entryname) 108 | .content(content) 109 | .build()); 110 | 111 | Diary persist2 = entityManager.persist(Diary.builder() 112 | .username(username) 113 | .entryname(entryname+"1") 114 | .content(content) 115 | .build()); 116 | 117 | Diary persist3 = entityManager.persist(Diary.builder() 118 | .username(username+"1") 119 | .entryname(entryname) 120 | .content(content) 121 | .build()); 122 | 123 | List list = repository.findAll(); 124 | assertTrue(list.size() == 3); 125 | assertTrue(list.get(0).getId().equals(persist1.getId())); 126 | assertTrue(list.get(1).getId().equals(persist2.getId())); 127 | assertTrue(list.get(2).getId().equals(persist3.getId())); 128 | } 129 | 130 | @Test 131 | public void findByUsernameAndEntrynameShouldSuccessTest() { 132 | Diary persist1 = entityManager.persist(Diary.builder() 133 | .username(username) 134 | .entryname(entryname) 135 | .content(content) 136 | .build()); 137 | 138 | entityManager.persist(Diary.builder() 139 | .username(username) 140 | .entryname(entryname+"1") 141 | .content(content) 142 | .build()); 143 | 144 | entityManager.persist(Diary.builder() 145 | .username(username+"1") 146 | .entryname(entryname) 147 | .content(content) 148 | .build()); 149 | 150 | Optional diary = repository.findByUsernameAndEntryname(persist1.getUsername(), persist1.getEntryname()); 151 | assertTrue(diary.isPresent()); 152 | assertThat(diary.get().getUsername(), is(equalTo(username))); 153 | assertThat(diary.get().getEntryname(), is(equalTo(entryname))); 154 | assertThat(diary.get().getContent(), is(equalTo(content))); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/domain/UserRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.domain; 2 | 3 | import static org.hamcrest.Matchers.equalTo; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.junit.Assert.assertThat; 6 | import static org.junit.Assert.assertTrue; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 16 | import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | 19 | import com.github.maxamel.server.domain.model.User; 20 | import com.github.maxamel.server.domain.repositories.UserRepository; 21 | 22 | @RunWith(SpringRunner.class) 23 | @DataJpaTest 24 | public class UserRepositoryTest { 25 | 26 | @Autowired 27 | private TestEntityManager entityManager; 28 | 29 | @Autowired 30 | private UserRepository repository; 31 | 32 | @Value("{test.username}") 33 | private String username; 34 | 35 | @Value("${test.passwordless}") 36 | private String pass; 37 | 38 | @Test 39 | public void findOneShouldSuccessTest() { 40 | User persist = entityManager.persist(User.builder() 41 | .name(username) 42 | .passwordless(pass) 43 | .build()); 44 | 45 | Optional user = repository.findOne(persist.getId()); 46 | assertTrue(user.isPresent()); 47 | assertThat(user.get().getName(), is(equalTo(username))); 48 | assertThat(user.get().getPasswordless(), is(equalTo(pass))); 49 | } 50 | 51 | @Test 52 | public void findByNameShouldSuccessTest() { 53 | User persist = entityManager.persist(User.builder() 54 | .name(username) 55 | .passwordless(pass) 56 | .build()); 57 | 58 | Optional user = repository.findByName(persist.getName()); 59 | assertTrue(user.isPresent()); 60 | assertThat(user.get().getName(), is(equalTo(username))); 61 | assertThat(user.get().getPasswordless(), is(equalTo(pass))); 62 | } 63 | 64 | @Test 65 | public void removeShouldSuccessTest() { 66 | User persist = entityManager.persist(User.builder() 67 | .name(username) 68 | .passwordless(pass) 69 | .build()); 70 | 71 | repository.delete(persist.getId()); 72 | Optional user = repository.findOne(persist.getId()); 73 | assertTrue(!user.isPresent()); 74 | 75 | } 76 | 77 | @Test 78 | public void removeByNameShouldSuccessTest() { 79 | User persist = entityManager.persist(User.builder() 80 | .name(username) 81 | .passwordless(pass) 82 | .build()); 83 | 84 | repository.deleteByName(persist.getName()); 85 | Optional user = repository.findOne(persist.getId()); 86 | assertTrue(!user.isPresent()); 87 | 88 | } 89 | 90 | @Test 91 | public void findAllShouldSuccessTest() { 92 | User persist1 = entityManager.persist(User.builder() 93 | .name(username) 94 | .passwordless(pass) 95 | .build()); 96 | 97 | User persist2 = entityManager.persist(User.builder() 98 | .name("Mike") 99 | .passwordless(pass) 100 | .build()); 101 | 102 | List users = repository.findAll(); 103 | 104 | assertTrue(users.size() == 2); 105 | assertTrue(users.get(0).equals(persist1)); 106 | assertTrue(users.get(1).equals(persist2)); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/services/DiaryServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | import static org.mockito.Mockito.when; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.Matchers; 13 | import org.mockito.Mockito; 14 | import org.mockito.invocation.InvocationOnMock; 15 | import org.mockito.stubbing.Answer; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.beans.factory.annotation.Value; 18 | import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer; 19 | import org.springframework.boot.test.mock.mockito.MockBean; 20 | import org.springframework.test.context.ContextConfiguration; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | 23 | import com.github.maxamel.server.domain.model.Diary; 24 | import com.github.maxamel.server.domain.repositories.DiaryRepository; 25 | import com.github.maxamel.server.services.impl.DiaryServiceImpl; 26 | import com.github.maxamel.server.services.mapping.MappingBasePackage; 27 | import com.github.maxamel.server.web.dtos.DiaryDto; 28 | import com.github.rozidan.springboot.modelmapper.WithModelMapper; 29 | 30 | 31 | @RunWith(SpringRunner.class) 32 | @WithModelMapper(basePackageClasses = MappingBasePackage.class) 33 | @ContextConfiguration(classes = DiaryServiceImpl.class, initializers = ConfigFileApplicationContextInitializer.class) 34 | public class DiaryServiceTest { 35 | 36 | @Autowired 37 | private DiaryService diaryService; 38 | 39 | @MockBean 40 | private UserService userService; 41 | 42 | @MockBean 43 | private DiaryRepository repository; 44 | 45 | @Value("${test.passwordless}") 46 | private String pass; 47 | 48 | @Value("${test.entryname}") 49 | private String entryname; 50 | 51 | @Value("{test.username}") 52 | private String username; 53 | 54 | @Test 55 | public void removeByUsernameAndEntryname() 56 | { 57 | Mockito.doAnswer(new Answer() { 58 | @Override 59 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 60 | Object[] args = invocationOnMock.getArguments(); 61 | String toBeDeleted1 = (String) args[0]; 62 | String toBeDeleted2 = (String) args[1]; 63 | assertTrue(toBeDeleted1.equals(username)); 64 | assertTrue(toBeDeleted2.equals(entryname)); 65 | return null; 66 | } 67 | }).when(repository).deleteByUsernameAndEntryname(Matchers.any(String.class), Matchers.any(String.class)); 68 | diaryService.removeByUsernameAndEntryname(username, entryname, pass); 69 | } 70 | 71 | @Test 72 | public void removeAll() 73 | { 74 | Diary result1 = Diary.builder() 75 | .id(1L) 76 | .username(username) 77 | .entryname(entryname+"1") 78 | .content(pass) 79 | .build(); 80 | 81 | Diary result2 = Diary.builder() 82 | .id(2L) 83 | .username(username) 84 | .entryname(entryname+"2") 85 | .content(pass) 86 | .build(); 87 | 88 | List list = Arrays.asList(result1 , result2); 89 | when(repository.findByUsername(Matchers.any(String.class))).thenReturn(list); 90 | 91 | Mockito.doAnswer(new Answer() { 92 | @Override 93 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 94 | Object[] args = invocationOnMock.getArguments(); 95 | String toBeDeleted1 = (String) args[0]; 96 | assertTrue(toBeDeleted1.equals(username)); 97 | return null; 98 | } 99 | }).when(repository).deleteByUsernameAndEntryname(Matchers.any(String.class), Matchers.any(String.class)); 100 | diaryService.removeAll(username, pass); 101 | } 102 | 103 | @Test 104 | public void add() 105 | { 106 | Diary intermidiate = Diary.builder() 107 | .id(1L) 108 | .username(username) 109 | .entryname(entryname) 110 | .content(pass) 111 | .build(); 112 | DiaryDto result = DiaryDto.builder() 113 | .id(1L) 114 | .username(username) 115 | .entryname(entryname) 116 | .content(pass) 117 | .build(); 118 | Optional opt = Optional.empty(); 119 | 120 | when(repository.findByUsernameAndEntryname(Matchers.any(String.class), Matchers.any(String.class))).thenReturn(opt); 121 | when(repository.save(Matchers.any(Diary.class))).thenReturn(intermidiate); 122 | DiaryDto dto = diaryService.add(result, pass); 123 | assertTrue(dto.getId().equals(result.getId())); 124 | assertTrue(dto.getUsername().equals(result.getUsername())); 125 | assertTrue(dto.getEntryname().equals(result.getEntryname())); 126 | assertTrue(dto.getContent().equals(result.getContent())); 127 | } 128 | 129 | @Test 130 | public void fetch() 131 | { 132 | Diary result = Diary.builder() 133 | .id(1L) 134 | .username(username) 135 | .entryname(entryname) 136 | .content(pass) 137 | .build(); 138 | Optional opt = Optional.of(result); 139 | 140 | when(repository.findByUsernameAndEntryname(Matchers.any(String.class), Matchers.any(String.class))).thenReturn(opt); 141 | DiaryDto dto = diaryService.fetch(username, entryname, pass); 142 | assertTrue(dto.getId().equals(result.getId())); 143 | assertTrue(dto.getUsername().equals(result.getUsername())); 144 | assertTrue(dto.getEntryname().equals(result.getEntryname())); 145 | assertTrue(dto.getContent().equals(result.getContent())); 146 | } 147 | 148 | @Test 149 | public void fetchByUsername() 150 | { 151 | Diary result1 = Diary.builder() 152 | .id(1L) 153 | .username(username) 154 | .entryname(entryname+"1") 155 | .content(pass) 156 | .build(); 157 | 158 | Diary result2 = Diary.builder() 159 | .id(2L) 160 | .username(username) 161 | .entryname(entryname+"2") 162 | .content(pass) 163 | .build(); 164 | 165 | List list = Arrays.asList(result1 , result2); 166 | 167 | when(repository.findByUsername(Matchers.any(String.class))).thenReturn(list); 168 | List dtos = diaryService.fetchByUsername(username, pass); 169 | assertTrue(dtos.get(0).getId().equals(result1.getId())); 170 | assertTrue(dtos.get(1).getId().equals(result2.getId())); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/services/KafkaAgentServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.BlockingQueue; 5 | import java.util.concurrent.LinkedBlockingQueue; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import org.apache.kafka.clients.consumer.ConsumerRecord; 9 | import org.apache.kafka.clients.producer.ProducerConfig; 10 | import org.apache.kafka.common.serialization.StringSerializer; 11 | import org.junit.After; 12 | import org.junit.Assert; 13 | import org.junit.Before; 14 | import org.junit.ClassRule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.boot.test.context.SpringBootTest; 19 | import org.springframework.boot.test.mock.mockito.MockBean; 20 | import org.springframework.kafka.core.DefaultKafkaConsumerFactory; 21 | import org.springframework.kafka.core.DefaultKafkaProducerFactory; 22 | import org.springframework.kafka.core.KafkaTemplate; 23 | import org.springframework.kafka.core.ProducerFactory; 24 | import org.springframework.kafka.listener.KafkaMessageListenerContainer; 25 | import org.springframework.kafka.listener.MessageListener; 26 | import org.springframework.kafka.listener.config.ContainerProperties; 27 | import org.springframework.kafka.support.serializer.JsonSerializer; 28 | import org.springframework.kafka.test.rule.KafkaEmbedded; 29 | import org.springframework.kafka.test.utils.ContainerTestUtils; 30 | import org.springframework.kafka.test.utils.KafkaTestUtils; 31 | import org.springframework.test.context.junit4.SpringRunner; 32 | 33 | import com.github.maxamel.server.services.impl.KafkaAgentServiceImpl; 34 | import com.github.maxamel.server.services.mapping.MappingBasePackage; 35 | import com.github.maxamel.server.web.dtos.ChallengeDto; 36 | import com.github.rozidan.springboot.modelmapper.WithModelMapper; 37 | 38 | 39 | @RunWith(SpringRunner.class) 40 | @WithModelMapper(basePackageClasses = MappingBasePackage.class) 41 | @SpringBootTest 42 | @SuppressWarnings("PMD.SingularField") 43 | public class KafkaAgentServiceTest { 44 | 45 | private KafkaAgentService sender; 46 | 47 | @MockBean 48 | private KafkaAgentService kafka; 49 | 50 | @Autowired 51 | private Map producerConfigs; 52 | 53 | @Autowired 54 | private ProducerFactory producerFactory; 55 | 56 | private final static String topic = "topic"; 57 | 58 | private KafkaMessageListenerContainer container; 59 | 60 | private BlockingQueue> records; 61 | 62 | @ClassRule 63 | public static KafkaEmbedded embeddedKafka = new KafkaEmbedded(1, true, topic); 64 | 65 | @Before 66 | public void setUp() throws Exception { 67 | // set up the Kafka consumer properties 68 | Map consumerProperties = 69 | KafkaTestUtils.consumerProps("sender", "false", embeddedKafka); 70 | 71 | // create a Kafka consumer factory 72 | DefaultKafkaConsumerFactory consumerFactory = 73 | new DefaultKafkaConsumerFactory(consumerProperties); 74 | 75 | // set the topic that needs to be consumed 76 | ContainerProperties containerProperties = new ContainerProperties(topic); 77 | 78 | // create a Kafka MessageListenerContainer 79 | container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties); 80 | 81 | // create a thread safe queue to store the received message 82 | records = new LinkedBlockingQueue<>(); 83 | 84 | // setup a Kafka message listener 85 | container.setupMessageListener(new MessageListener() { 86 | @Override 87 | public void onMessage(ConsumerRecord record) { 88 | records.add(record); 89 | } 90 | }); 91 | 92 | // start the container and underlying message listener 93 | container.start(); 94 | 95 | producerConfigs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, embeddedKafka.getBrokersAsString()); 96 | producerConfigs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 97 | producerConfigs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); 98 | 99 | producerFactory = new DefaultKafkaProducerFactory(producerConfigs); 100 | KafkaTemplate template = new KafkaTemplate<>(producerFactory); 101 | // wait until the container has the required number of assigned partitions 102 | ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic()); 103 | 104 | sender = new KafkaAgentServiceImpl(embeddedKafka.getZookeeperConnectionString(), template); 105 | } 106 | 107 | @After 108 | public void tearDown() { 109 | // stop the container 110 | container.stop(); 111 | } 112 | 113 | @Test 114 | public void testSend() throws InterruptedException { 115 | 116 | // send the message 117 | ChallengeDto dto = ChallengeDto.builder().challenge("challenge").build(); 118 | sender.send(topic, dto); 119 | 120 | // check that the message was received 121 | ConsumerRecord received = records.poll(10, TimeUnit.SECONDS); 122 | // Hamcrest Matchers to check the value 123 | Assert.assertTrue(received.topic().equals(topic)); 124 | Assert.assertTrue(received.key() == null); 125 | Assert.assertTrue(received.value().contains("challenge")); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/services/ScheduleTaskServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services; 2 | 3 | import com.github.maxamel.server.domain.model.User; 4 | import com.github.maxamel.server.domain.model.types.SessionStatus; 5 | import com.github.maxamel.server.domain.repositories.UserRepository; 6 | import com.github.maxamel.server.services.impl.ScheduleTaskServiceImpl; 7 | import com.github.maxamel.server.services.mapping.MappingBasePackage; 8 | import com.github.rozidan.springboot.modelmapper.WithModelMapper; 9 | 10 | import org.junit.Assert; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Matchers; 14 | import org.mockito.Mockito; 15 | import org.mockito.invocation.InvocationOnMock; 16 | import org.mockito.stubbing.Answer; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.beans.factory.annotation.Value; 19 | import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer; 20 | import org.springframework.boot.test.mock.mockito.MockBean; 21 | import org.springframework.test.context.ContextConfiguration; 22 | import org.springframework.test.context.junit4.SpringRunner; 23 | 24 | import static org.mockito.Mockito.any; 25 | import static org.mockito.Mockito.when; 26 | 27 | import java.util.HashMap; 28 | import java.util.Map; 29 | import java.util.Optional; 30 | import java.util.concurrent.Executors; 31 | import java.util.concurrent.ScheduledExecutorService; 32 | 33 | @RunWith(SpringRunner.class) 34 | @WithModelMapper(basePackageClasses = MappingBasePackage.class) 35 | @ContextConfiguration(classes = ScheduleTaskServiceImpl.class, initializers = ConfigFileApplicationContextInitializer.class) 36 | 37 | public class ScheduleTaskServiceTest { 38 | 39 | @MockBean 40 | private UserService service; 41 | 42 | @MockBean 43 | private UserRepository repository; 44 | 45 | @MockBean 46 | private KafkaAgentService kafka; 47 | 48 | @Autowired 49 | private ScheduleTaskService scheduler; 50 | 51 | 52 | @Value("${test.passwordless}") 53 | private String pass; 54 | 55 | @Value("${test.secret}") 56 | private String sec; 57 | 58 | @Value("${test.wrongsecret}") 59 | private String wsec; 60 | 61 | @Value("${test.answer}") 62 | private String answer; 63 | 64 | @Value("{test.username}") 65 | private String username; 66 | 67 | @Test 68 | public void publishChallengeValidatedUser() 69 | { 70 | User result = User.builder() 71 | .id(1L) 72 | .name(username) 73 | .passwordless(pass) 74 | .secret(sec) 75 | .sstatus(SessionStatus.VALIDATED) 76 | .build(); 77 | Optional opt = Optional.of(result); 78 | 79 | when(repository.findByName(any(String.class))).thenReturn(opt); 80 | 81 | Mockito.doAnswer(new Answer() { 82 | @Override 83 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 84 | Object[] args = invocationOnMock.getArguments(); 85 | User toBeSaved = (User) args[0]; 86 | System.out.println(toBeSaved.getSecret().length()); 87 | Assert.assertTrue(!toBeSaved.getSecret().equals(sec)); 88 | return null; 89 | } 90 | }).when(repository).save(Matchers.any(User.class)); 91 | scheduler.publishChallenge(result); 92 | } 93 | 94 | @Test 95 | public void handleActivityWaitingUser() 96 | { 97 | Map timers = new HashMap<>(); 98 | User result = User.builder() 99 | .id(1L) 100 | .name(username) 101 | .passwordless(pass) 102 | .secret(sec) 103 | .sstatus(SessionStatus.WAITING) 104 | .build(); 105 | Optional opt = Optional.of(result); 106 | ScheduledExecutorService execService = Executors.newScheduledThreadPool(2); 107 | timers.put(result.getId(), execService); 108 | when(repository.findByName(any(String.class))).thenReturn(opt); 109 | 110 | Mockito.doAnswer(new Answer() { 111 | @Override 112 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 113 | Object[] args = invocationOnMock.getArguments(); 114 | User toBeSaved = (User) args[0]; 115 | Assert.assertTrue(toBeSaved.getSecret() == null); 116 | Assert.assertTrue(toBeSaved.getSstatus().equals(SessionStatus.INVALIDATED)); 117 | return null; 118 | } 119 | }).when(repository).save(Matchers.any(User.class)); 120 | scheduler.handleActivity(result, timers); 121 | 122 | } 123 | 124 | @Test 125 | public void handleActivityInitiatingUser() 126 | { 127 | Map timers = new HashMap<>(); 128 | User result = User.builder() 129 | .id(1L) 130 | .name(username) 131 | .passwordless(pass) 132 | .secret(sec) 133 | .sstatus(SessionStatus.INITIATING) 134 | .build(); 135 | Optional opt = Optional.of(result); 136 | ScheduledExecutorService execService = Executors.newScheduledThreadPool(2); 137 | timers.put(result.getId(), execService); 138 | when(repository.findByName(any(String.class))).thenReturn(opt); 139 | 140 | Mockito.doAnswer(new Answer() { 141 | @Override 142 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 143 | Object[] args = invocationOnMock.getArguments(); 144 | User toBeSaved = (User) args[0]; 145 | Assert.assertTrue(toBeSaved.getSecret() == null); 146 | Assert.assertTrue(toBeSaved.getSstatus().equals(SessionStatus.INVALIDATED)); 147 | return null; 148 | } 149 | }).when(repository).save(Matchers.any(User.class)); 150 | scheduler.handleActivity(result, timers); 151 | 152 | } 153 | 154 | @Test 155 | public void handleActivityValidatedUser() 156 | { 157 | Map timers = new HashMap<>(); 158 | User result = User.builder() 159 | .id(1L) 160 | .name(username) 161 | .passwordless(pass) 162 | .secret(sec) 163 | .sstatus(SessionStatus.VALIDATED) 164 | .build(); 165 | Optional opt = Optional.of(result); 166 | ScheduledExecutorService execService = Executors.newScheduledThreadPool(2); 167 | timers.put(result.getId(), execService); 168 | 169 | when(repository.findByName(any(String.class))).thenReturn(opt); 170 | 171 | Mockito.doAnswer(new Answer() { 172 | @Override 173 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 174 | Object[] args = invocationOnMock.getArguments(); 175 | User toBeSaved = (User) args[0]; 176 | Assert.assertTrue(toBeSaved.getSecret().equals(sec)); 177 | Assert.assertTrue(toBeSaved.getSstatus().equals(SessionStatus.WAITING)); 178 | return null; 179 | } 180 | }).when(repository).save(Matchers.any(User.class)); 181 | scheduler.handleActivity(result, timers); 182 | 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/services/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services; 2 | 3 | import com.github.maxamel.server.domain.model.User; 4 | import com.github.maxamel.server.domain.model.types.SessionStatus; 5 | import com.github.maxamel.server.domain.repositories.UserRepository; 6 | import com.github.maxamel.server.services.impl.UserServiceImpl; 7 | import com.github.maxamel.server.services.mapping.MappingBasePackage; 8 | import com.github.maxamel.server.web.dtos.UserDto; 9 | import com.github.rozidan.springboot.modelmapper.WithModelMapper; 10 | 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Matchers; 14 | import org.mockito.Mockito; 15 | import org.mockito.invocation.InvocationOnMock; 16 | import org.mockito.stubbing.Answer; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.beans.factory.annotation.Value; 19 | import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer; 20 | import org.springframework.boot.test.mock.mockito.MockBean; 21 | import org.springframework.dao.DataIntegrityViolationException; 22 | import org.springframework.dao.EmptyResultDataAccessException; 23 | import org.springframework.security.access.AccessDeniedException; 24 | import org.springframework.test.context.ContextConfiguration; 25 | import org.springframework.test.context.junit4.SpringRunner; 26 | 27 | import static org.junit.Assert.assertTrue; 28 | import static org.mockito.Mockito.any; 29 | import static org.mockito.Mockito.when; 30 | 31 | import java.lang.reflect.InvocationTargetException; 32 | import java.lang.reflect.Method; 33 | import java.util.Optional; 34 | 35 | @RunWith(SpringRunner.class) 36 | @WithModelMapper(basePackageClasses = MappingBasePackage.class) 37 | @ContextConfiguration(classes = UserServiceImpl.class, initializers = ConfigFileApplicationContextInitializer.class) 38 | 39 | public class UserServiceTest { 40 | 41 | @Autowired 42 | private UserService service; 43 | 44 | @MockBean 45 | private UserRepository repository; 46 | 47 | @MockBean 48 | private ScheduleTaskService scheduler; 49 | 50 | @MockBean 51 | private DiaryService diaryService; 52 | 53 | @MockBean 54 | private KafkaAgentService kafkaService; 55 | 56 | @Value("${test.passwordless}") 57 | private String pass; 58 | 59 | @Value("${test.secret}") 60 | private String sec; 61 | 62 | @Value("${test.wrongsecret}") 63 | private String wsec; 64 | 65 | @Value("${test.answer}") 66 | private String answer; 67 | 68 | @Value("{test.username}") 69 | private String username; 70 | 71 | @Test 72 | public void fetch() 73 | { 74 | User result = User.builder() 75 | .id(1L) 76 | .name(username) 77 | .passwordless(pass) 78 | .secret(sec) 79 | .sstatus(SessionStatus.WAITING) 80 | .build(); 81 | Optional opt = Optional.of(result); 82 | 83 | when(repository.findByName(any(String.class))).thenReturn(opt); 84 | UserDto dto = service.fetch(username, answer); 85 | assertTrue(dto.getId().equals(result.getId())); 86 | assertTrue(dto.getName().equals(result.getName())); 87 | assertTrue(dto.getPasswordless().equals(result.getPasswordless())); 88 | assertTrue(dto.getSecret().equals(result.getSecret())); 89 | assertTrue(dto.getSstatus().equals(result.getSstatus())); 90 | assertTrue(dto.getSstatus().equals(SessionStatus.byValue(1))); 91 | assertTrue(dto.getSstatus().getValue().equals(1)); 92 | } 93 | 94 | @Test 95 | public void register() 96 | { 97 | User intermidiate = User.builder() 98 | .id(1L) 99 | .name(username) 100 | .passwordless(pass) 101 | .secret(sec) 102 | .sstatus(SessionStatus.WAITING) 103 | .build(); 104 | UserDto result = UserDto.builder() 105 | .id(1L) 106 | .name(username) 107 | .passwordless(pass) 108 | .secret(sec) 109 | .sstatus(SessionStatus.WAITING) 110 | .build(); 111 | Optional opt = Optional.empty(); 112 | 113 | when(repository.findByName(any(String.class))).thenReturn(opt); 114 | when(repository.save(any(User.class))).thenReturn(intermidiate); 115 | UserDto dto = service.register(result); 116 | assertTrue(dto.getId().equals(result.getId())); 117 | assertTrue(dto.getName().equals(result.getName())); 118 | assertTrue(dto.getPasswordless().equals(result.getPasswordless())); 119 | assertTrue(dto.getSecret().equals(result.getSecret())); 120 | assertTrue(dto.getSstatus().equals(result.getSstatus())); 121 | } 122 | 123 | 124 | @Test 125 | public void removeByName() 126 | { 127 | User result = User.builder() 128 | .id(1L) 129 | .name(username) 130 | .passwordless(pass) 131 | .secret(sec) 132 | .sstatus(SessionStatus.WAITING) 133 | .build(); 134 | Optional opt = Optional.of(result); 135 | 136 | when(repository.findByName(any(String.class))).thenReturn(opt); 137 | 138 | Mockito.doAnswer(new Answer() { 139 | @Override 140 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 141 | Object[] args = invocationOnMock.getArguments(); 142 | String toBeDeleted = (String) args[0]; 143 | assertTrue(toBeDeleted.equals(result.getName())); 144 | return null; 145 | } 146 | }).when(repository).deleteByName(Matchers.any(String.class)); 147 | service.removeByName(result.getName(),answer); 148 | } 149 | 150 | @Test 151 | public void fetchWaitToValidate() 152 | { 153 | User result = User.builder() 154 | .id(1L) 155 | .name(username) 156 | .passwordless(pass) 157 | .secret(sec) 158 | .sstatus(SessionStatus.WAITING) 159 | .build(); 160 | Optional opt = Optional.of(result); 161 | 162 | when(repository.findByName(any(String.class))).thenReturn(opt); 163 | UserDto dto = service.fetch(username, answer); 164 | assertTrue(dto.getSstatus().equals(SessionStatus.VALIDATED)); 165 | } 166 | 167 | @Test(expected = AccessDeniedException.class) 168 | public void fetchAuthenticationError() 169 | { 170 | User result = User.builder() 171 | .id(1L) 172 | .name(username) 173 | .passwordless(pass) 174 | .secret(wsec) 175 | .sstatus(SessionStatus.WAITING) 176 | .build(); 177 | Optional opt = Optional.of(result); 178 | 179 | when(repository.findByName(any(String.class))).thenReturn(opt); 180 | service.fetch(username, answer); 181 | } 182 | 183 | @Test(expected = AccessDeniedException.class) 184 | public void fetchNoSecret() 185 | { 186 | User result = User.builder() 187 | .id(1L) 188 | .name(username) 189 | .passwordless(pass) 190 | .secret(null) 191 | .sstatus(SessionStatus.WAITING) 192 | .build(); 193 | Optional opt = Optional.of(result); 194 | 195 | when(repository.findByName(any(String.class))).thenReturn(opt); 196 | service.fetch(username, answer); 197 | } 198 | 199 | @Test(expected = AccessDeniedException.class) 200 | public void removeByNameNoSecret() 201 | { 202 | User result = User.builder() 203 | .id(1L) 204 | .name(username) 205 | .passwordless(pass) 206 | .secret(null) 207 | .sstatus(SessionStatus.WAITING) 208 | .build(); 209 | Optional opt = Optional.of(result); 210 | 211 | when(repository.findByName(any(String.class))).thenReturn(opt); 212 | service.removeByName(username,answer); 213 | } 214 | 215 | @Test(expected = EmptyResultDataAccessException.class) 216 | public void removeNonExistentUser() 217 | { 218 | when(repository.findByName(any(String.class))).thenThrow(new EmptyResultDataAccessException("No user found with name: " + username, 1)); 219 | service.removeByName(username, answer); 220 | } 221 | 222 | 223 | @Test(expected = DataIntegrityViolationException.class) 224 | public void findByNameExistingUser() { 225 | UserDto user = UserDto.builder() 226 | .id(1L) 227 | .name(username) 228 | .passwordless(pass) 229 | .secret(sec) 230 | .sstatus(SessionStatus.WAITING) 231 | .build(); 232 | 233 | when(repository.findByName(any(String.class))).thenThrow(new DataIntegrityViolationException("User already exists with name:" + user.getName())); 234 | service.register(user); 235 | } 236 | 237 | @Test 238 | public void verifyCorrect() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException 239 | { 240 | User user = User.builder() 241 | .id(1L) 242 | .name(username) 243 | .passwordless(pass) 244 | .secret(sec) 245 | .sstatus(SessionStatus.INITIATING) 246 | .build(); 247 | 248 | Class c = UserServiceImpl.class; 249 | Class[] cArgs = new Class[2]; 250 | cArgs[0] = User.class; 251 | cArgs[1] = String.class; 252 | Method method = c.getDeclaredMethod("verify", cArgs); 253 | method.setAccessible(true); 254 | Boolean obj = (Boolean) method.invoke(service, user, answer); 255 | assertTrue(user.getSstatus().equals(SessionStatus.VALIDATED)); 256 | assertTrue(obj.booleanValue()); 257 | } 258 | 259 | @Test 260 | public void verifyIncorrect() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException 261 | { 262 | User user = User.builder() 263 | .id(1L) 264 | .name(username) 265 | .passwordless(pass) 266 | .secret(wsec) 267 | .sstatus(SessionStatus.WAITING) 268 | .build(); 269 | 270 | Class c = UserServiceImpl.class; 271 | Class[] cArgs = new Class[2]; 272 | cArgs[0] = User.class; 273 | cArgs[1] = String.class; 274 | Method method = c.getDeclaredMethod("verify", cArgs); 275 | method.setAccessible(true); 276 | Boolean obj = (Boolean) method.invoke(service, user, answer); 277 | assertTrue(user.getSstatus().equals(SessionStatus.INVALIDATED)); 278 | assertTrue(user.getSecret() == null); 279 | assertTrue(!obj.booleanValue()); 280 | } 281 | 282 | @Test 283 | public void generateServerSecret() 284 | { 285 | User input = User.builder() 286 | .id(1L) 287 | .name(username) 288 | .passwordless(pass) 289 | .secret(sec) 290 | .sstatus(SessionStatus.WAITING) 291 | .build(); 292 | User result = User.builder() 293 | .id(1L) 294 | .name(username) 295 | .passwordless(pass) 296 | .secret(sec) 297 | .sstatus(SessionStatus.WAITING) 298 | .build(); 299 | Optional opt = Optional.of(result); 300 | 301 | when(repository.findByName(any(String.class))).thenReturn(opt); 302 | 303 | Mockito.doAnswer(new Answer() { 304 | @Override 305 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 306 | Object[] args = invocationOnMock.getArguments(); 307 | User toBeUpdated = (User) args[0]; 308 | assertTrue(toBeUpdated.getName().equals(input.getName())); 309 | assertTrue(!toBeUpdated.getSecret().equals(input.getSecret())); 310 | assertTrue(toBeUpdated.getSstatus().equals(SessionStatus.INITIATING)); 311 | return null; 312 | } 313 | }).when(repository).save(Matchers.any(User.class)); 314 | service.generateServerSecret(input); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/services/mapping/MappingBasePackage.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services.mapping; 2 | 3 | public final class MappingBasePackage { 4 | } 5 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/services/mapping/MappingValidateTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services.mapping; 2 | 3 | import com.github.rozidan.springboot.modelmapper.WithModelMapper; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.modelmapper.ModelMapper; 7 | import org.modelmapper.ValidationException; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | import static org.junit.Assert.fail; 12 | 13 | @RunWith(SpringRunner.class) 14 | @WithModelMapper(basePackageClasses = MappingBasePackage.class) 15 | public class MappingValidateTest { 16 | 17 | @Autowired 18 | private ModelMapper mapper; 19 | 20 | @Test 21 | public void mapperValidationShouldSuccess() { 22 | try { 23 | mapper.validate(); 24 | 25 | } catch (ValidationException e) { 26 | fail(e.getMessage()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/services/mapping/UserMappingTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.services.mapping; 2 | 3 | import com.github.maxamel.server.web.dtos.UserDto; 4 | import com.github.maxamel.server.domain.model.User; 5 | import com.github.rozidan.springboot.modelmapper.WithModelMapper; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.modelmapper.ModelMapper; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.equalTo; 15 | import static org.hamcrest.Matchers.is; 16 | 17 | @RunWith(SpringRunner.class) 18 | @WithModelMapper(basePackageClasses = MappingBasePackage.class) 19 | public class UserMappingTest { 20 | 21 | @Autowired 22 | private ModelMapper mapper; 23 | 24 | @Test 25 | public void productDtoToEntityMappedSuccess() { 26 | 27 | UserDto dto = UserDto.builder() 28 | .id(1L) 29 | .name("John") 30 | .passwordless("455632178871263") 31 | .build(); 32 | 33 | User result = mapper.map(dto, User.class); 34 | 35 | assertThat(result.getId(), is(equalTo(1L))); 36 | assertThat(result.getName(), is(equalTo("John"))); 37 | assertThat(result.getPasswordless(), is(equalTo("455632178871263"))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/web/DiaryControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web; 2 | 3 | import com.github.maxamel.server.web.dtos.DiaryDto; 4 | import com.github.maxamel.server.web.dtos.ErrorCodes; 5 | import com.github.maxamel.server.config.JsonConfiguration; 6 | import com.github.maxamel.server.domain.model.constraints.UserEntryNameUnique; 7 | import com.github.maxamel.server.domain.model.constraints.UserNameUnique; 8 | import com.github.maxamel.server.services.DiaryService; 9 | import com.github.maxamel.server.services.UserService; 10 | import com.github.maxamel.server.web.controllers.DiaryController; 11 | import com.fasterxml.jackson.databind.ObjectWriter; 12 | import org.hibernate.exception.ConstraintViolationException; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 18 | import org.springframework.boot.test.mock.mockito.MockBean; 19 | import org.springframework.context.annotation.ComponentScan; 20 | import org.springframework.dao.DataIntegrityViolationException; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.test.context.ContextConfiguration; 23 | import org.springframework.test.context.junit4.SpringRunner; 24 | import org.springframework.test.web.servlet.MockMvc; 25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 26 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 27 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 28 | 29 | import static org.hamcrest.Matchers.equalTo; 30 | import static org.hamcrest.Matchers.is; 31 | import static org.mockito.Mockito.*; 32 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 34 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 35 | 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | 39 | @RunWith(SpringRunner.class) 40 | @ComponentScan(basePackageClasses = UserNameUnique.class) 41 | @ContextConfiguration(classes = JsonConfiguration.class) 42 | @WebMvcTest(secure = false, controllers = DiaryController.class) 43 | @SuppressWarnings("PMD.TooManyStaticImports") 44 | public class DiaryControllerTest { 45 | 46 | @Autowired 47 | private ObjectWriter writer; 48 | 49 | @Autowired 50 | private MockMvc mvc; 51 | 52 | @MockBean 53 | private DiaryService service; 54 | 55 | @MockBean 56 | private UserService userService; 57 | 58 | @Value("{test.username}") 59 | private String username; 60 | 61 | @Value("{test.entryname}") 62 | private String entryname; 63 | 64 | @Value("${test.passwordless}") 65 | private String pass; 66 | 67 | private final static String diary = "/diary"; 68 | private final static String diaryUser = "/diary/John/"; 69 | private final static String diaryUserEntry = "/diary/John/MyDiary/"; 70 | private final static String errorCode = "$.errorCode"; 71 | private final static String errors = "$.errors"; 72 | 73 | @Test 74 | public void addSuccess() throws Exception { 75 | DiaryDto request = DiaryDto.builder() 76 | .id(1L) 77 | .username(username) 78 | .entryname(entryname) 79 | .content(pass) 80 | .build(); 81 | 82 | DiaryDto result = DiaryDto.builder() 83 | .id(1L) 84 | .username(username) 85 | .entryname(entryname) 86 | .content(pass) 87 | .build(); 88 | 89 | when(service.add(any(DiaryDto.class), any(String.class))).thenReturn(result); 90 | mvc.perform(post(diary) 91 | .content(writer.writeValueAsString(request)) 92 | .contentType(MediaType.APPLICATION_JSON)) 93 | .andDo(print()) 94 | .andExpect(status().isCreated()) 95 | .andExpect(jsonPath("$.id", is(equalTo(1)))) 96 | .andExpect(jsonPath("$.username", is(equalTo(username)))) 97 | .andExpect(jsonPath("$.entryname", is(equalTo(entryname)))) 98 | .andExpect(jsonPath("$.content", is(equalTo(pass)))); 99 | 100 | verify(service, times(1)).add(any(DiaryDto.class), any(String.class)); 101 | verifyNoMoreInteractions(service); 102 | 103 | } 104 | 105 | @Test 106 | public void registerValidationFailedUniqueName() throws Exception { 107 | DiaryDto request = DiaryDto.builder() 108 | .id(1L) 109 | .username(username) 110 | .entryname(entryname) 111 | .content(pass) 112 | .build(); 113 | 114 | when(service.add(any(DiaryDto.class), any(String.class))) 115 | .thenThrow(new DataIntegrityViolationException("", 116 | new ConstraintViolationException("", null, UserEntryNameUnique.CONSTRAINT_NAME))); 117 | mvc.perform(post(diary) 118 | .content(writer.writeValueAsString(request)) 119 | .contentType(MediaType.APPLICATION_JSON)) 120 | .andDo(print()) 121 | .andExpect(status().isConflict()) 122 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.DATA_VALIDATION.toString())))) 123 | .andExpect(jsonPath(errors).isArray()) 124 | .andExpect(jsonPath("$.errors[0].fieldName", is(equalTo("username, entryname")))) 125 | .andExpect(jsonPath("$.errors[0].errorCode", is(equalTo("UNIQUE")))); 126 | 127 | verify(service, times(1)).add(any(DiaryDto.class), any(String.class)); 128 | verifyNoMoreInteractions(service); 129 | } 130 | 131 | @Test 132 | public void fetchSuccess() throws Exception { 133 | 134 | DiaryDto result = DiaryDto.builder() 135 | .id(1L) 136 | .username(username) 137 | .entryname(entryname) 138 | .content(pass) 139 | .build(); 140 | 141 | List list = new ArrayList<>(); 142 | list.add(result); 143 | 144 | when(service.fetchByUsername(any(String.class), any(String.class))).thenReturn(list); 145 | mvc.perform(get(diaryUser) 146 | .contentType(MediaType.APPLICATION_JSON)) 147 | .andDo(print()) 148 | .andExpect(status().isOk()) 149 | .andExpect(jsonPath("$[0].id", is(equalTo(1)))) 150 | .andExpect(jsonPath("$[0].username", is(equalTo(username)))) 151 | .andExpect(jsonPath("$[0].entryname", is(equalTo(entryname)))) 152 | .andExpect(jsonPath("$[0].content", is(equalTo(pass)))); 153 | 154 | verify(service, times(1)).fetchByUsername(any(String.class), any(String.class)); 155 | verifyNoMoreInteractions(service); 156 | } 157 | 158 | @Test 159 | public void removeSuccess() throws Exception { 160 | doNothing().when(service).removeByUsernameAndEntryname(any(String.class), any(String.class), any(String.class)); 161 | mvc.perform(delete(diaryUserEntry) 162 | .contentType(MediaType.APPLICATION_JSON)) 163 | .andDo(print()) 164 | .andExpect(status().isOk()); 165 | 166 | verify(service, times(1)).removeByUsernameAndEntryname(any(String.class), any(String.class), any(String.class)); 167 | verifyNoMoreInteractions(service); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/test/java/com/github/maxamel/server/web/UserControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maxamel.server.web; 2 | 3 | import com.github.maxamel.server.web.dtos.ChallengeDto; 4 | import com.github.maxamel.server.web.dtos.ErrorCodes; 5 | import com.github.maxamel.server.web.dtos.UserDto; 6 | import com.github.maxamel.server.config.JsonConfiguration; 7 | import com.github.maxamel.server.domain.model.constraints.UserNameUnique; 8 | import com.github.maxamel.server.services.UserService; 9 | import com.github.maxamel.server.web.controllers.UserController; 10 | import com.fasterxml.jackson.databind.ObjectWriter; 11 | import org.hibernate.exception.ConstraintViolationException; 12 | import org.hibernate.exception.LockAcquisitionException; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 18 | import org.springframework.boot.test.mock.mockito.MockBean; 19 | import org.springframework.context.annotation.ComponentScan; 20 | import org.springframework.dao.DataIntegrityViolationException; 21 | import org.springframework.dao.EmptyResultDataAccessException; 22 | import org.springframework.http.MediaType; 23 | import org.springframework.security.access.AccessDeniedException; 24 | import org.springframework.test.context.ContextConfiguration; 25 | import org.springframework.test.context.junit4.SpringRunner; 26 | import org.springframework.test.web.servlet.MockMvc; 27 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 28 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 29 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 30 | 31 | import static org.hamcrest.Matchers.equalTo; 32 | import static org.hamcrest.Matchers.is; 33 | import static org.mockito.Mockito.*; 34 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 35 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 36 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 37 | 38 | @RunWith(SpringRunner.class) 39 | @ComponentScan(basePackageClasses = UserNameUnique.class) 40 | @ContextConfiguration(classes = JsonConfiguration.class) 41 | @WebMvcTest(secure = false, controllers = UserController.class) 42 | @SuppressWarnings("PMD.TooManyStaticImports") 43 | public class UserControllerTest { 44 | 45 | @Autowired 46 | private ObjectWriter writer; 47 | 48 | @Autowired 49 | private MockMvc mvc; 50 | 51 | @MockBean 52 | private UserService service; 53 | 54 | @Value("{test.username}") 55 | private String username; 56 | 57 | @Value("${test.passwordless}") 58 | private String pass; 59 | 60 | private final static String users = "/users"; 61 | private final static String usersMike = "/users/Mike"; 62 | private final static String NOT_FOUND = "Not found"; 63 | private final static String message = "$.message"; 64 | private final static String errorCode = "$.errorCode"; 65 | private final static String errors = "$.errors"; 66 | 67 | @Test 68 | public void registerSuccess() throws Exception { 69 | UserDto request = UserDto.builder() 70 | .id(1L) 71 | .name(username) 72 | .passwordless(pass) 73 | .build(); 74 | 75 | UserDto result = UserDto.builder() 76 | .id(1L) 77 | .name(username) 78 | .passwordless(pass) 79 | .build(); 80 | 81 | when(service.register(any(UserDto.class))).thenReturn(result); 82 | mvc.perform(post(users) 83 | .content(writer.writeValueAsString(request)) 84 | .contentType(MediaType.APPLICATION_JSON)) 85 | .andDo(print()) 86 | .andExpect(status().isCreated()) 87 | .andExpect(jsonPath("$.id", is(equalTo(1)))) 88 | .andExpect(jsonPath("$.name", is(equalTo(username)))) 89 | .andExpect(jsonPath("$.passwordless", is(equalTo(pass)))); 90 | 91 | verify(service, times(1)).register(any(UserDto.class)); 92 | verifyNoMoreInteractions(service); 93 | 94 | } 95 | 96 | @Test 97 | public void UnknownPath() throws Exception { 98 | UserDto request = UserDto.builder() 99 | .id(1L) 100 | .name(username) 101 | .passwordless(pass) 102 | .build(); 103 | 104 | mvc.perform(post("/unknown") 105 | .content(writer.writeValueAsString(request)) 106 | .contentType(MediaType.APPLICATION_JSON)) 107 | .andDo(print()) 108 | .andExpect(status().isNotFound()) 109 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.NOT_FOUND.toString())))) 110 | .andExpect(jsonPath(errors).isEmpty()); 111 | } 112 | 113 | @Test 114 | public void IncorrectMethod() throws Exception { 115 | UserDto request = UserDto.builder() 116 | .id(1L) 117 | .name(username) 118 | .passwordless(pass) 119 | .build(); 120 | 121 | mvc.perform(get(users) 122 | .content(writer.writeValueAsString(request)) 123 | .contentType(MediaType.APPLICATION_JSON)) 124 | .andDo(print()) 125 | .andExpect(status().isMethodNotAllowed()) 126 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.METHOD_NOT_ALLOWED.toString())))) 127 | .andExpect(jsonPath(errors).isArray()) 128 | .andExpect(jsonPath("$.errors[0].actualMethod", is(equalTo("GET")))) 129 | .andExpect(jsonPath("$.errors[0].supportedMethods").isArray()) 130 | .andExpect(jsonPath("$.errors[0].supportedMethods[0]", is(equalTo("POST")))); 131 | } 132 | 133 | @Test 134 | public void InternalServerError() throws Exception { 135 | ChallengeDto request = ChallengeDto.builder().challenge(pass).build(); 136 | 137 | mvc.perform(post(users) 138 | .content(writer.writeValueAsString(request)) 139 | .contentType(MediaType.APPLICATION_JSON)) 140 | .andDo(print()) 141 | .andExpect(status().isInternalServerError()) 142 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.UNKNOWN.toString())))) 143 | .andExpect(jsonPath(errors).isArray()); 144 | } 145 | 146 | @Test 147 | public void UnsupportedMedia() throws Exception { 148 | UserDto request = UserDto.builder() 149 | .id(1L) 150 | .name(username) 151 | .passwordless(pass) 152 | .build(); 153 | 154 | mvc.perform(post(users) 155 | .content(writer.writeValueAsString(request)) 156 | .contentType(MediaType.APPLICATION_FORM_URLENCODED)) 157 | .andDo(print()) 158 | .andExpect(status().isUnsupportedMediaType()) 159 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.HTTP_MEDIA_TYPE_NOT_SUPPORTED.toString())))) 160 | .andExpect(jsonPath(errors).isArray()) 161 | .andExpect(jsonPath("$.errors[0].mediaType", is(equalTo("application/x-www-form-urlencoded")))); 162 | } 163 | 164 | @Test 165 | public void registerValidationFailedUniqueName() throws Exception { 166 | UserDto request = UserDto.builder() 167 | .id(1L) 168 | .name(username) 169 | .passwordless(pass) 170 | .build(); 171 | 172 | when(service.register(any(UserDto.class))) 173 | .thenThrow(new DataIntegrityViolationException("", 174 | new ConstraintViolationException("", null, UserNameUnique.CONSTRAINT_NAME))); 175 | mvc.perform(post(users) 176 | .content(writer.writeValueAsString(request)) 177 | .contentType(MediaType.APPLICATION_JSON)) 178 | .andDo(print()) 179 | .andExpect(status().isConflict()) 180 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.DATA_VALIDATION.toString())))) 181 | .andExpect(jsonPath(errors).isArray()) 182 | .andExpect(jsonPath("$.errors[0].fieldName", is(equalTo("name")))) 183 | .andExpect(jsonPath("$.errors[0].errorCode", is(equalTo("UNIQUE")))); 184 | 185 | verify(service, times(1)).register(any(UserDto.class)); 186 | verifyNoMoreInteractions(service); 187 | } 188 | 189 | @Test 190 | public void dataIntegrityViolation() throws Exception { 191 | UserDto request = UserDto.builder() 192 | .id(1L) 193 | .name(username) 194 | .passwordless(pass) 195 | .build(); 196 | 197 | when(service.register(any(UserDto.class))) 198 | .thenThrow(new DataIntegrityViolationException("", 199 | new LockAcquisitionException("", null, UserNameUnique.CONSTRAINT_NAME))); 200 | mvc.perform(post(users) 201 | .content(writer.writeValueAsString(request)) 202 | .contentType(MediaType.APPLICATION_JSON)) 203 | .andDo(print()) 204 | .andExpect(status().isConflict()) 205 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.UNKNOWN.toString())))) 206 | .andExpect(jsonPath(errors).isArray()); 207 | 208 | verify(service, times(1)).register(any(UserDto.class)); 209 | verifyNoMoreInteractions(service); 210 | } 211 | @Test 212 | public void fetchSuccess() throws Exception { 213 | UserDto result = UserDto.builder() 214 | .id(1L) 215 | .name(username) 216 | .passwordless(pass) 217 | .build(); 218 | 219 | when(service.fetch(any(String.class), any(String.class))).thenReturn(result); 220 | mvc.perform(get(usersMike) 221 | .contentType(MediaType.APPLICATION_JSON)) 222 | .andDo(print()) 223 | .andExpect(status().isOk()) 224 | .andExpect(jsonPath("$.id", is(equalTo(1)))) 225 | .andExpect(jsonPath("$.name", is(equalTo(username)))) 226 | .andExpect(jsonPath("$.passwordless", is(equalTo(pass)))); 227 | 228 | verify(service, times(1)).fetch(any(String.class), any(String.class)); 229 | verifyNoMoreInteractions(service); 230 | } 231 | 232 | @Test 233 | public void fetchNotFound() throws Exception { 234 | when(service.fetch(any(String.class), any(String.class))) 235 | .thenThrow(new EmptyResultDataAccessException("Not found",0)); 236 | mvc.perform(get(usersMike) 237 | .contentType(MediaType.APPLICATION_JSON)) 238 | .andDo(print()) 239 | .andExpect(status().isNotFound()) 240 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.NOT_FOUND.toString())))) 241 | .andExpect(jsonPath(message, is(equalTo(NOT_FOUND)))); 242 | 243 | verify(service, times(1)).fetch(any(String.class), any(String.class)); 244 | verifyNoMoreInteractions(service); 245 | } 246 | 247 | @Test 248 | public void fetchWrongSessionId() throws Exception { 249 | when(service.fetch(any(String.class), any(String.class))) 250 | .thenThrow(new AccessDeniedException(pass)); 251 | mvc.perform(get(usersMike) 252 | .contentType(MediaType.APPLICATION_JSON)) 253 | .andDo(print()) 254 | .andExpect(status().isUnauthorized()) 255 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.UNAUTHORIZED.toString())))) 256 | .andExpect(jsonPath("$.challenge", is(equalTo(pass)))) 257 | .andExpect(jsonPath(message, is(equalTo("Unauthorized")))); 258 | 259 | verify(service, times(1)).fetch(any(String.class), any(String.class)); 260 | verifyNoMoreInteractions(service); 261 | } 262 | 263 | @Test 264 | public void removeSuccess() throws Exception { 265 | doNothing().when(service).removeByName(any(String.class), any(String.class)); 266 | mvc.perform(delete(usersMike) 267 | .contentType(MediaType.APPLICATION_JSON)) 268 | .andDo(print()) 269 | .andExpect(status().isOk()); 270 | 271 | verify(service, times(1)).removeByName(any(String.class), any(String.class)); 272 | verifyNoMoreInteractions(service); 273 | } 274 | 275 | @Test 276 | public void removeNotFound() throws Exception { 277 | doThrow(new EmptyResultDataAccessException("Not found",0)).when(service).removeByName(any(String.class), any(String.class)); 278 | mvc.perform(delete(usersMike) 279 | .contentType(MediaType.APPLICATION_JSON)) 280 | .andDo(print()) 281 | .andExpect(status().isNotFound()) 282 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.NOT_FOUND.toString())))) 283 | .andExpect(jsonPath(message, is(equalTo(NOT_FOUND)))); 284 | 285 | verify(service, times(1)).removeByName(any(String.class), any(String.class)); 286 | verifyNoMoreInteractions(service); 287 | } 288 | 289 | @Test 290 | public void removeWrongSessionId() throws Exception { 291 | doThrow(new AccessDeniedException(pass)).when(service).removeByName(any(String.class), any(String.class)); 292 | mvc.perform(delete(usersMike) 293 | .contentType(MediaType.APPLICATION_JSON)) 294 | .andDo(print()) 295 | .andExpect(status().isUnauthorized()) 296 | .andExpect(jsonPath(errorCode, is(equalTo(ErrorCodes.UNAUTHORIZED.toString())))) 297 | .andExpect(jsonPath("$.challenge", is(equalTo(pass)))) 298 | .andExpect(jsonPath(message, is(equalTo("Unauthorized")))); 299 | 300 | verify(service, times(1)).removeByName(any(String.class), any(String.class)); 301 | verifyNoMoreInteractions(service); 302 | } 303 | } 304 | --------------------------------------------------------------------------------