├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __snapshot__ ├── create a developer if it's a karumi developer.snap └── retrieve by id.snap ├── _config.yml ├── admin-server ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── kotlin │ └── com │ │ └── karumi │ │ └── admin │ │ └── server │ │ └── AdminServerApplication.kt │ └── resources │ └── application.properties ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── src ├── main │ ├── kotlin │ │ └── com │ │ │ └── karumi │ │ │ └── springbootkotlin │ │ │ ├── Application.kt │ │ │ ├── SwaggerConfig.kt │ │ │ ├── authentication │ │ │ ├── api │ │ │ │ ├── AuthController.kt │ │ │ │ └── model.kt │ │ │ └── domain │ │ │ │ └── usecase │ │ │ │ ├── LoginDeveloper.kt │ │ │ │ └── RegisterDeveloper.kt │ │ │ ├── common │ │ │ ├── TryLogger.kt │ │ │ └── extensions.kt │ │ │ ├── developers │ │ │ ├── api │ │ │ │ ├── DeveloperController.kt │ │ │ │ └── model.kt │ │ │ ├── domain │ │ │ │ ├── DeveloperValidator.kt │ │ │ │ ├── model.kt │ │ │ │ └── usecase │ │ │ │ │ ├── CreateKarumiDeveloper.kt │ │ │ │ │ └── GetDeveloper.kt │ │ │ └── storage │ │ │ │ ├── DeveloperDao.kt │ │ │ │ └── entity.kt │ │ │ ├── security │ │ │ ├── CustomUserDetailsService.kt │ │ │ ├── JwtAuthenticationEntryPoint.kt │ │ │ ├── SecurityConfig.kt │ │ │ ├── SecurityUser.kt │ │ │ ├── TokenAuthenticationFilter.kt │ │ │ └── TokenHelper.kt │ │ │ └── workers │ │ │ └── SimpleScheduler.kt │ ├── requests │ │ ├── Actuator.http │ │ ├── Authentication.http │ │ ├── DeveloperController.http │ │ └── http-client.env.json │ └── resources │ │ └── application.properties └── test │ ├── kotlin │ └── com │ │ └── karumi │ │ └── springbootkotlin │ │ ├── MockCustomUser.kt │ │ ├── controllers │ │ ├── DeveloperControllerTest.kt │ │ ├── DeveloperControllerTestMockUser.kt │ │ └── DeveloperControllerUsingMvcMock.kt │ │ ├── developers │ │ └── storage │ │ │ ├── DeveloperRepositoryTest.kt │ │ │ └── kotlintestconfig.kt │ │ ├── extensions.kt │ │ └── given │ │ └── GivenDeveloper.kt │ └── resources │ ├── BadDeveloperBody.json │ ├── CreateDeveloper.json │ ├── CreateKarumiDeveloper.json │ ├── ExpectedDeveloper.json │ ├── ExpectedNewDeveloper.json │ └── LoginRequest.json └── travis.yml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{java, kt, kts, scala, rs, xml}] 8 | indent_size = 2 9 | continuation_indent_size = 2 10 | insert_final_newline = false 11 | max_line_length = 100 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | logs 23 | target 24 | /.idea 25 | /.idea_modules 26 | /.classpath 27 | /.project 28 | /.settings 29 | .vscode 30 | /RUNNING_PID 31 | **/.DS_Store 32 | .gradle/ 33 | build 34 | out/ 35 | .kotlintest 36 | 37 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 38 | hs_err_pid* 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | before_install: 5 | - chmod +x gradlew 6 | - chmod +x gradle/wrapper/gradle-wrapper.jar 7 | script: 8 | - ./gradlew ktlint test 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Karumi logo][karumilogo] Spring Boot Kotlin 2 | 3 | [Spring boot][springboot] Kotlin example using some Spring features like Security, Actuator, JPA, Workers and MvcTests. 4 | 5 | Application code example it's based on [Play Framework Kotlin repository][playframeworkkotlin] with some news endpoints 6 | to register and login developers, it uses H2 in-memory database. 7 | 8 | 9 | Running 10 | ================ 11 | ``` 12 | ./gradlew bootRun 13 | ``` 14 | 15 | By default the local port is `5000`, you can change it at the [application.properties][properties] file. 16 | 17 | Usage 18 | ================ 19 | You can use [IntelliJ Http Client][httpclient] files to make some request to the local server [located here][httpfiles]. 20 | Run `Authentication.http` file before others to have credentials to make requests. 21 | 22 | You can also take a look at all endpoints in Swagger UI Web at http://localhost:5000/swagger-ui.html. 23 | 24 | Actuator 25 | ================ 26 | [Actuator][actuator] brings production-ready features to monitor our application, gathering metrics, traffic, database metrics, etc... 27 | It uses HTTP endpoints to enable us to interact with it. 28 | 29 | We use [codemetric/spring-boot-admin][codemetric] library to provide an admin web interface using Actuator endpoints in a 30 | separate server, to run admin panel web: 31 | 32 | ``` 33 | cd admin-server 34 | ./gradlew bootRun 35 | ``` 36 | 37 | Which is listening on `http://localhost:8080/` with user `admin` and password `admin-password`. 38 | 39 | 40 | License 41 | ------- 42 | 43 | Copyright 2018 Karumi 44 | 45 | Licensed under the Apache License, Version 2.0 (the "License"); 46 | you may not use this file except in compliance with the License. 47 | You may obtain a copy of the License at 48 | 49 | http://www.apache.org/licenses/LICENSE-2.0 50 | 51 | Unless required by applicable law or agreed to in writing, software 52 | distributed under the License is distributed on an "AS IS" BASIS, 53 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 54 | See the License for the specific language governing permissions and 55 | limitations under the License. 56 | 57 | [karumilogo]: https://cloud.githubusercontent.com/assets/858090/11626547/e5a1dc66-9ce3-11e5-908d-537e07e82090.png 58 | [codemetric]: https://github.com/codecentric/spring-boot-admin 59 | [actuator]: https://www.baeldung.com/spring-boot-actuators 60 | [properties]: https://github.com/Karumi/SpringBootKotlin/blob/master/src/main/resources/application.properties 61 | [httpfiles]: https://github.com/Karumi/SpringBootKotlin/tree/master/src/main/requests 62 | [httpclient]: https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html 63 | [playframeworkkotlin]: https://github.com/Karumi/play-framework-kotlin 64 | [springboot]: https://spring.io/projects/spring-boot 65 | -------------------------------------------------------------------------------- /__snapshot__/create a developer if it's a karumi developer.snap: -------------------------------------------------------------------------------- 1 | {"id":"c04ecc99-d7ba-460c-be78-6995805c5175","username":"Unknown","email":"email@karumi.com"} -------------------------------------------------------------------------------- /__snapshot__/retrieve by id.snap: -------------------------------------------------------------------------------- 1 | {"id":"c04ecc99-d7ba-460c-be78-6995805c5175","username":"Unknown","email":"email@karumi.com"} -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: "SpringBootKotlin" 2 | baseurl: /SpringBootKotlin 3 | remote_theme: Karumi/KarumiJekyllTheme 4 | -------------------------------------------------------------------------------- /admin-server/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | /out/ 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | -------------------------------------------------------------------------------- /admin-server/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.1.2.RELEASE' 3 | id 'org.jetbrains.kotlin.jvm' version '1.2.71' 4 | id 'org.jetbrains.kotlin.plugin.spring' version '1.2.71' 5 | } 6 | 7 | apply plugin: 'io.spring.dependency-management' 8 | 9 | group = 'com.karumi.admin.server' 10 | version = '0.0.1-SNAPSHOT' 11 | sourceCompatibility = '1.8' 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 20 | implementation 'org.springframework.boot:spring-boot-starter-security' 21 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' 22 | implementation 'de.codecentric:spring-boot-admin-starter-server:2.1.2' 23 | runtimeOnly 'org.springframework.boot:spring-boot-devtools' 24 | 25 | } 26 | 27 | compileKotlin { 28 | kotlinOptions { 29 | freeCompilerArgs = ['-Xjsr305=strict'] 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | compileTestKotlin { 35 | kotlinOptions { 36 | freeCompilerArgs = ['-Xjsr305=strict'] 37 | jvmTarget = '1.8' 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /admin-server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karumi/SpringBootKotlin/f6d16b7ed43cc5d8077287514cd5c30962d84b29/admin-server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /admin-server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Feb 18 02:05:01 CET 2019 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.10.2-all.zip 7 | -------------------------------------------------------------------------------- /admin-server/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 | -------------------------------------------------------------------------------- /admin-server/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 | -------------------------------------------------------------------------------- /admin-server/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | } 6 | rootProject.name = 'admin-server' 7 | -------------------------------------------------------------------------------- /admin-server/src/main/kotlin/com/karumi/admin/server/AdminServerApplication.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.admin.server 2 | 3 | import de.codecentric.boot.admin.server.config.EnableAdminServer 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | import org.springframework.boot.runApplication 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 8 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 9 | import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler 10 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository 11 | 12 | @SpringBootApplication 13 | @EnableAdminServer 14 | class AdminServerApplication 15 | 16 | fun main(args: Array) { 17 | runApplication(*args) 18 | } 19 | 20 | @Configuration 21 | class ActuatorSecurityConfig : WebSecurityConfigurerAdapter() { 22 | 23 | @Throws(Exception::class) 24 | override fun configure(http: HttpSecurity) { 25 | val successHandler = SavedRequestAwareAuthenticationSuccessHandler() 26 | successHandler.setTargetUrlParameter("redirectTo") 27 | successHandler.setDefaultTargetUrl("/") 28 | 29 | http.authorizeRequests() 30 | .antMatchers("/assets/**").permitAll() 31 | .antMatchers("/login").permitAll() 32 | .anyRequest().authenticated() 33 | .and() 34 | .formLogin().loginPage("/login").successHandler(successHandler).and() 35 | .logout().logoutUrl("/logout").and() 36 | .httpBasic().and() 37 | .csrf() 38 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 39 | .ignoringAntMatchers( 40 | "/instances", 41 | "/actuator/**" 42 | ) 43 | } 44 | } -------------------------------------------------------------------------------- /admin-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.security.user.name=admin 2 | spring.security.user.password=admin-password 3 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.3.21' 4 | springBootVersion = '2.1.3.RELEASE' 5 | } 6 | repositories { 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 11 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 12 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 13 | classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}") 14 | classpath('com.karumi.kotlinsnapshot:plugin:2.0.0') 15 | } 16 | } 17 | 18 | apply plugin: 'kotlin' 19 | apply plugin: 'kotlin-spring' 20 | apply plugin: 'kotlin-jpa' 21 | apply plugin: 'kotlin-allopen' 22 | apply plugin: 'org.springframework.boot' 23 | apply plugin: 'io.spring.dependency-management' 24 | apply plugin: 'com.karumi.kotlin-snapshot' 25 | 26 | 27 | group 'spring-boot-kotlin' 28 | version '1.0-SNAPSHOT' 29 | 30 | repositories { 31 | mavenCentral() 32 | } 33 | 34 | sourceCompatibility = 1.8 35 | compileKotlin { 36 | kotlinOptions { 37 | freeCompilerArgs = ["-Xjsr305=strict"] 38 | jvmTarget = "1.8" 39 | } 40 | } 41 | compileTestKotlin { 42 | kotlinOptions { 43 | freeCompilerArgs = ["-Xjsr305=strict"] 44 | jvmTarget = "1.8" 45 | } 46 | } 47 | 48 | configurations { 49 | ktlint 50 | developmentOnly 51 | runtimeClasspath { 52 | extendsFrom developmentOnly 53 | } 54 | } 55 | 56 | allOpen { 57 | annotation('javax.persistence.Entity') 58 | } 59 | 60 | test { 61 | useJUnitPlatform() 62 | } 63 | 64 | dependencies { 65 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 66 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 67 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 68 | implementation 'org.springframework.boot:spring-boot-starter-web' 69 | implementation 'org.springframework.boot:spring-boot-starter-security' 70 | 71 | def swaggerVersion = "2.9.2" 72 | implementation "io.springfox:springfox-swagger2:$swaggerVersion" 73 | implementation "io.springfox:springfox-swagger-ui:$swaggerVersion" 74 | implementation "io.springfox:springfox-bean-validators:$swaggerVersion" 75 | implementation "org.apache.httpcomponents:httpclient:4.5.2" 76 | 77 | implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' 78 | implementation "de.codecentric:spring-boot-admin-starter-client:2.1.2" 79 | 80 | def arrowVersion = "0.8.2" 81 | implementation "io.arrow-kt:arrow-core:$arrowVersion" 82 | implementation "io.arrow-kt:arrow-instances-core:$arrowVersion" 83 | 84 | implementation "io.jsonwebtoken:jjwt:0.9.1" 85 | 86 | def kotlintestVersion = "3.3.0-RC3" 87 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 88 | exclude module: 'junit' 89 | } 90 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.2.0' 91 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.2.0' 92 | testImplementation "io.kotlintest:kotlintest-runner-junit5:$kotlintestVersion" 93 | testImplementation "io.kotlintest:kotlintest-assertions-arrow:$kotlintestVersion" 94 | testImplementation "io.kotlintest:kotlintest-extensions-spring:$kotlintestVersion" 95 | testImplementation 'com.ninja-squad:springmockk:1.1.0' 96 | testImplementation 'org.springframework.security:spring-security-test:5.1.4.RELEASE' 97 | 98 | runtimeOnly 'org.springframework.boot:spring-boot-devtools' 99 | runtimeOnly 'com.h2database:h2' 100 | 101 | ktlint "com.github.shyiko:ktlint:0.31.0" 102 | } 103 | 104 | task ktlint(type: JavaExec) { 105 | main = "com.github.shyiko.ktlint.Main" 106 | classpath = configurations.ktlint 107 | args "src/**/*.kt" 108 | } 109 | 110 | check.dependsOn ktlint 111 | 112 | task ktlintFormat(type: JavaExec) { 113 | main = "com.github.shyiko.ktlint.Main" 114 | classpath = configurations.ktlint 115 | args "-F", "src/**/*.kt" 116 | } 117 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karumi/SpringBootKotlin/f6d16b7ed43cc5d8077287514cd5c30962d84b29/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 23 18:01:08 CET 2019 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.8-all.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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-boot-kotlin' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/Application.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.scheduling.annotation.EnableAsync 7 | import org.springframework.scheduling.annotation.EnableScheduling 8 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor 9 | import java.util.concurrent.Executor 10 | 11 | @SpringBootApplication 12 | @EnableAsync 13 | @EnableScheduling 14 | class Application { 15 | 16 | @Bean 17 | fun taskExecutor(): Executor { 18 | val executor = ThreadPoolTaskExecutor() 19 | executor.corePoolSize = 2 20 | executor.maxPoolSize = 2 21 | executor.setQueueCapacity(500) 22 | executor.setThreadNamePrefix("Scheduler-") 23 | executor.initialize() 24 | return executor 25 | } 26 | } 27 | 28 | fun main(args: Array) { 29 | runApplication(*args) 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/SwaggerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.http.HttpHeaders.AUTHORIZATION 6 | import org.springframework.security.core.Authentication 7 | import springfox.documentation.builders.PathSelectors 8 | import springfox.documentation.builders.RequestHandlerSelectors 9 | import springfox.documentation.service.ApiKey 10 | import springfox.documentation.service.AuthorizationScope 11 | import springfox.documentation.service.SecurityReference 12 | import springfox.documentation.spi.DocumentationType 13 | import springfox.documentation.spi.service.contexts.SecurityContext 14 | import springfox.documentation.spring.web.plugins.Docket 15 | import springfox.documentation.swagger.web.ApiKeyVehicle.HEADER 16 | import springfox.documentation.swagger2.annotations.EnableSwagger2 17 | 18 | @Configuration 19 | @EnableSwagger2 20 | class SwaggerConfig { 21 | 22 | companion object { 23 | private const val API_KEY_NAME = "Token Access" 24 | } 25 | 26 | @Bean 27 | fun api(): Docket = Docket(DocumentationType.SWAGGER_2) 28 | .ignoredParameterTypes(Authentication::class.java) 29 | .select() 30 | .apis(RequestHandlerSelectors.basePackage("com.karumi.springbootkotlin")) 31 | .paths(PathSelectors.any()) 32 | .build() 33 | .enable(true) 34 | .securityContexts(listOf(securityContext())) 35 | .securitySchemes(listOf(apiKey())) 36 | 37 | private fun apiKey(): ApiKey = ApiKey(API_KEY_NAME, AUTHORIZATION, HEADER.name) 38 | 39 | private fun securityContext(): SecurityContext { 40 | return SecurityContext.builder() 41 | .securityReferences(defaultAuth()) 42 | .forPaths(PathSelectors.any()) 43 | .build() 44 | } 45 | 46 | fun defaultAuth(): List { 47 | val authScope = AuthorizationScope("global", "accessEverything") 48 | val authorizationScopes = arrayOf(authScope) 49 | return listOf(SecurityReference(API_KEY_NAME, authorizationScopes)) 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/authentication/api/AuthController.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.authentication.api 2 | 3 | import com.karumi.springbootkotlin.authentication.domain.usecase.LoginDeveloper 4 | import com.karumi.springbootkotlin.authentication.domain.usecase.RegisterDeveloper 5 | import com.karumi.springbootkotlin.common.orThrow 6 | import com.karumi.springbootkotlin.developers.api.DeveloperBody 7 | import com.karumi.springbootkotlin.developers.api.toBody 8 | import com.karumi.springbootkotlin.developers.domain.PasswordEncoder 9 | import org.springframework.http.HttpStatus 10 | import org.springframework.http.ResponseEntity 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken 12 | import org.springframework.web.bind.annotation.PostMapping 13 | import org.springframework.web.bind.annotation.RequestBody 14 | import org.springframework.web.bind.annotation.RequestMapping 15 | import org.springframework.web.bind.annotation.RestController 16 | 17 | @RestController 18 | @RequestMapping("/auth") 19 | class AuthController( 20 | private val registerDeveloper: RegisterDeveloper, 21 | private val loginDeveloper: LoginDeveloper, 22 | private val passwordEncoder: PasswordEncoder 23 | ) { 24 | 25 | @PostMapping("/register") 26 | fun register(@RequestBody request: NewDeveloperRequest): ResponseEntity { 27 | val user = request.toDomain(passwordEncoder) 28 | return registerDeveloper(user) 29 | .map { ResponseEntity(it.toBody(), HttpStatus.CREATED) } 30 | .orThrow() 31 | } 32 | 33 | @PostMapping("/login") 34 | fun login(@RequestBody request: LoginRequest): String { 35 | val userPass = UsernamePasswordAuthenticationToken(request.username, request.password) 36 | return loginDeveloper(userPass).orThrow() 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/authentication/api/model.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.authentication.api 2 | 3 | import com.karumi.springbootkotlin.developers.domain.Developer 4 | import com.karumi.springbootkotlin.developers.domain.PasswordEncoder 5 | 6 | data class NewDeveloperRequest( 7 | val username: String, 8 | val email: String?, 9 | val password: String 10 | ) 11 | 12 | data class LoginRequest( 13 | val username: String, 14 | val password: String 15 | ) 16 | 17 | fun NewDeveloperRequest.toDomain(encoder: PasswordEncoder): Developer = Developer( 18 | username = username, 19 | email = email, 20 | password = encoder(password) 21 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/authentication/domain/usecase/LoginDeveloper.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.authentication.domain.usecase 2 | 3 | import arrow.core.Try 4 | import com.karumi.springbootkotlin.common.TryLogger 5 | import com.karumi.springbootkotlin.security.SetAuthentication 6 | import com.karumi.springbootkotlin.security.TokenHelper 7 | import org.springframework.security.authentication.AuthenticationManager 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken 9 | import org.springframework.stereotype.Component 10 | 11 | @Component 12 | class LoginDeveloper( 13 | private val authenticationManager: AuthenticationManager, 14 | private val tokenHelper: TokenHelper, 15 | private val setAuthentication: SetAuthentication 16 | ) { 17 | 18 | operator fun invoke(userPass: UsernamePasswordAuthenticationToken): Try = TryLogger { 19 | val authentication = authenticationManager.authenticate(userPass) 20 | setAuthentication(authentication) 21 | tokenHelper.generateToken(userPass.principal.toString()) 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/authentication/domain/usecase/RegisterDeveloper.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.authentication.domain.usecase 2 | 3 | import arrow.core.Option 4 | import arrow.core.Try 5 | import arrow.core.failure 6 | import com.karumi.springbootkotlin.developers.domain.AlreadyRegistered 7 | import com.karumi.springbootkotlin.developers.domain.Developer 8 | import com.karumi.springbootkotlin.developers.storage.DeveloperDao 9 | import org.springframework.stereotype.Component 10 | 11 | @Component 12 | class RegisterDeveloper( 13 | private val developerDao: DeveloperDao 14 | ) { 15 | 16 | operator fun invoke(developer: Developer): Try = 17 | developerDao.getByUsername(developer.username) 18 | .flatMap(createIfNotRegistered(developer)) 19 | 20 | private fun createIfNotRegistered(developer: Developer): (Option) -> Try = { 21 | it.fold({ 22 | developerDao.create(developer) 23 | }, { 24 | AlreadyRegistered.failure() 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/common/TryLogger.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.common 2 | 3 | import arrow.core.Try 4 | import java.util.logging.Level 5 | import java.util.logging.Logger 6 | 7 | inline fun A.TryLogger(f: () -> B): Try = Try(f).apply { 8 | fold( 9 | { Try { Logger.getLogger(A::class.qualifiedName).log(Level.SEVERE, it.message, it) } }, 10 | { Unit } 11 | ) 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/common/extensions.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.common 2 | 3 | import arrow.core.Option 4 | import arrow.core.Try 5 | import arrow.core.failure 6 | import arrow.core.identity 7 | import arrow.core.success 8 | 9 | fun Try.mapException(map: (exception: Throwable) -> Throwable): Try = 10 | fold({ map(it).failure() }, { it.success() }) 11 | 12 | fun Try.orThrow(): A = fold({ throw it }, ::identity) 13 | 14 | fun Option.toTry(ifEmpty: () -> Throwable): Try = 15 | fold({ ifEmpty().failure() }, { it.success() }) -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/developers/api/DeveloperController.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.api 2 | 3 | import com.karumi.springbootkotlin.authentication.api.NewDeveloperRequest 4 | import com.karumi.springbootkotlin.authentication.api.toDomain 5 | import com.karumi.springbootkotlin.common.orThrow 6 | import com.karumi.springbootkotlin.developers.domain.PasswordEncoder 7 | import com.karumi.springbootkotlin.developers.domain.usecase.CreateKarumiDeveloper 8 | import com.karumi.springbootkotlin.developers.domain.usecase.GetDeveloper 9 | import org.springframework.http.HttpStatus.CREATED 10 | import org.springframework.http.ResponseEntity 11 | import org.springframework.scheduling.annotation.Async 12 | import org.springframework.security.core.Authentication 13 | import org.springframework.web.bind.annotation.GetMapping 14 | import org.springframework.web.bind.annotation.PathVariable 15 | import org.springframework.web.bind.annotation.PostMapping 16 | import org.springframework.web.bind.annotation.RequestBody 17 | import org.springframework.web.bind.annotation.RestController 18 | import java.util.UUID 19 | import java.util.concurrent.CompletableFuture 20 | import java.util.concurrent.CompletionStage 21 | import java.util.logging.Logger 22 | 23 | @RestController 24 | class DeveloperController( 25 | private val createKarumiDeveloper: CreateKarumiDeveloper, 26 | private val getKarumiDeveloper: GetDeveloper, 27 | private val passwordEncoder: PasswordEncoder 28 | ) { 29 | 30 | @GetMapping("/") 31 | fun index(): CompletionStage = asyncHello() 32 | 33 | @Async 34 | fun asyncHello(): CompletionStage { 35 | Logger.getLogger(DeveloperController::class.qualifiedName).info("Karumi") 36 | Thread.sleep(1000) 37 | return CompletableFuture.completedFuture("Karumi") 38 | } 39 | 40 | @PostMapping("/developer") 41 | fun createDeveloper( 42 | @RequestBody developer: NewDeveloperRequest, 43 | authentication: Authentication 44 | ): ResponseEntity = 45 | createKarumiDeveloper(developer.toDomain(passwordEncoder)) 46 | .map { ResponseEntity(it.toBody(), CREATED) } 47 | .orThrow() 48 | 49 | @GetMapping("/developer/{developerId}") 50 | fun getDeveloper(@PathVariable developerId: UUID, authentication: Authentication): DeveloperBody = 51 | getKarumiDeveloper(developerId) 52 | .map { it.toBody() } 53 | .orThrow() 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/developers/api/model.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.api 2 | 3 | import com.karumi.springbootkotlin.developers.domain.Developer 4 | 5 | data class DeveloperBody( 6 | val id: String, 7 | val username: String, 8 | val email: String? 9 | ) 10 | 11 | fun Developer.toBody(): DeveloperBody = DeveloperBody(id.toString(), username, email) -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/developers/domain/DeveloperValidator.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.domain 2 | 3 | object DeveloperValidator { 4 | private const val KARUMI_EMAIL = "@karumi.com" 5 | 6 | fun isKarumiDeveloper(developer: Developer): Boolean = 7 | developer.email != null && developer.email.contains(KARUMI_EMAIL) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/developers/domain/model.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.domain 2 | 3 | import org.springframework.http.HttpStatus 4 | import org.springframework.web.bind.annotation.ResponseStatus 5 | import java.util.UUID 6 | 7 | typealias 8 | PasswordEncoder = (String) -> String 9 | 10 | data class Developer( 11 | val username: String, 12 | val email: String?, 13 | val password: String, 14 | val id: UUID? = null 15 | ) 16 | 17 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 18 | object StorageError : RuntimeException("Internal storage error") 19 | 20 | @ResponseStatus(HttpStatus.NOT_FOUND) 21 | object NotFound : RuntimeException("Developer not found") 22 | 23 | @ResponseStatus(HttpStatus.BAD_REQUEST) 24 | object NotKarumier : RuntimeException("Developer isn't karumier") 25 | 26 | @ResponseStatus(HttpStatus.CONFLICT) 27 | object AlreadyRegistered : RuntimeException("Developer already registered") -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/developers/domain/usecase/CreateKarumiDeveloper.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.domain.usecase 2 | 3 | import arrow.core.Try 4 | import arrow.core.failure 5 | import arrow.core.success 6 | import com.karumi.springbootkotlin.common.mapException 7 | import com.karumi.springbootkotlin.developers.domain.Developer 8 | import com.karumi.springbootkotlin.developers.domain.DeveloperValidator 9 | import com.karumi.springbootkotlin.developers.domain.NotKarumier 10 | import com.karumi.springbootkotlin.developers.domain.StorageError 11 | import com.karumi.springbootkotlin.developers.storage.DeveloperDao 12 | import org.springframework.stereotype.Component 13 | 14 | @Component 15 | class CreateKarumiDeveloper( 16 | private val developerDao: DeveloperDao 17 | ) { 18 | 19 | operator fun invoke(developer: Developer): Try = 20 | validKarumiDeveloper(developer) 21 | .flatMap { 22 | developerDao.create(it) 23 | .mapException { StorageError } 24 | } 25 | 26 | private fun validKarumiDeveloper(developer: Developer): Try = 27 | if (DeveloperValidator.isKarumiDeveloper(developer)) { 28 | developer.success() 29 | } else { 30 | NotKarumier.failure() 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/developers/domain/usecase/GetDeveloper.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.domain.usecase 2 | 3 | import arrow.core.Try 4 | import com.karumi.springbootkotlin.common.mapException 5 | import com.karumi.springbootkotlin.common.toTry 6 | import com.karumi.springbootkotlin.developers.domain.Developer 7 | import com.karumi.springbootkotlin.developers.domain.NotFound 8 | import com.karumi.springbootkotlin.developers.domain.StorageError 9 | import com.karumi.springbootkotlin.developers.storage.DeveloperDao 10 | import org.springframework.stereotype.Component 11 | import java.util.UUID 12 | 13 | @Component 14 | class GetDeveloper( 15 | private val developerDao: DeveloperDao 16 | ) { 17 | 18 | operator fun invoke(developerId: UUID): Try = 19 | developerDao.getById(developerId) 20 | .mapException { StorageError } 21 | .flatMap { devOption -> devOption.toTry { NotFound } } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/developers/storage/DeveloperDao.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.storage 2 | 3 | import arrow.core.Option 4 | import arrow.core.Try 5 | import arrow.core.toOption 6 | import com.karumi.springbootkotlin.common.TryLogger 7 | import com.karumi.springbootkotlin.developers.domain.Developer 8 | import org.springframework.data.repository.CrudRepository 9 | import org.springframework.stereotype.Component 10 | import java.util.UUID 11 | 12 | @Component 13 | interface DeveloperRepository : CrudRepository { 14 | fun findByUsernameContainingIgnoreCase(username: String): DeveloperEntity? 15 | } 16 | 17 | @Component 18 | class DeveloperDao(private val developerRepository: DeveloperRepository) { 19 | 20 | fun create(developer: Developer): Try = TryLogger { 21 | developerRepository.save(developer.toEntity()).toDomain() 22 | } 23 | 24 | fun update(developer: Developer): Try = TryLogger { 25 | developerRepository.save(developer.toEntity()).toDomain() 26 | } 27 | 28 | fun getById(developerId: UUID): Try> = TryLogger { 29 | developerRepository.findById(developerId) 30 | .orElse(null) 31 | .toOption() 32 | .map { it.toDomain() } 33 | } 34 | 35 | fun getByUsername(username: String): Try> = TryLogger { 36 | developerRepository.findByUsernameContainingIgnoreCase(username) 37 | .toOption() 38 | .map { it.toDomain() } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/developers/storage/entity.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.storage 2 | 3 | import com.karumi.springbootkotlin.developers.domain.Developer 4 | import java.util.UUID 5 | import javax.persistence.Entity 6 | import javax.persistence.GeneratedValue 7 | import javax.persistence.GenerationType 8 | import javax.persistence.Id 9 | 10 | @Entity 11 | data class DeveloperEntity( 12 | val username: String, 13 | val email: String?, 14 | val password: String, 15 | @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: UUID? 16 | ) 17 | 18 | fun Developer.toEntity(): DeveloperEntity = DeveloperEntity(username, email, password, id) 19 | 20 | fun DeveloperEntity.toDomain(): Developer = Developer( 21 | username, email, password, id 22 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/security/CustomUserDetailsService.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.security 2 | 3 | import com.karumi.springbootkotlin.common.orThrow 4 | import com.karumi.springbootkotlin.common.toTry 5 | import com.karumi.springbootkotlin.developers.storage.DeveloperDao 6 | import org.springframework.security.core.userdetails.UserDetails 7 | import org.springframework.security.core.userdetails.UserDetailsService 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException 9 | import org.springframework.stereotype.Service 10 | 11 | @Service 12 | class CustomUserDetailsService( 13 | private val developerDao: DeveloperDao 14 | ) : UserDetailsService { 15 | 16 | override fun loadUserByUsername(username: String): UserDetails = 17 | developerDao.getByUsername(username) 18 | .flatMap { it.toTry { UsernameNotFoundException("User $username not found") } } 19 | .orThrow() 20 | .toSecurityUser() 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/security/JwtAuthenticationEntryPoint.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.security 2 | 3 | import org.springframework.security.core.AuthenticationException 4 | import org.springframework.security.web.AuthenticationEntryPoint 5 | import javax.servlet.http.HttpServletRequest 6 | import javax.servlet.http.HttpServletResponse 7 | 8 | class JwtAuthenticationEntryPoint : AuthenticationEntryPoint { 9 | override fun commence( 10 | request: HttpServletRequest, 11 | response: HttpServletResponse, 12 | authException: AuthenticationException 13 | ) { 14 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.localizedMessage) 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/security/SecurityConfig.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.security 2 | 3 | import com.karumi.springbootkotlin.developers.domain.PasswordEncoder 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.security.authentication.AuthenticationManager 8 | import org.springframework.security.config.BeanIds 9 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder 10 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 12 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 14 | import org.springframework.security.config.http.SessionCreationPolicy 15 | import org.springframework.security.core.Authentication 16 | import org.springframework.security.core.context.SecurityContextHolder 17 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 18 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 19 | 20 | typealias SetAuthentication = (Authentication) -> Unit 21 | 22 | @Configuration 23 | @EnableWebSecurity 24 | @EnableGlobalMethodSecurity(prePostEnabled = true) 25 | class SecurityConfig( 26 | private val userDetailsService: CustomUserDetailsService, 27 | private val tokenHelper: TokenHelper 28 | ) : WebSecurityConfigurerAdapter() { 29 | 30 | companion object { 31 | private val CRYPT_ENCODER = BCryptPasswordEncoder() 32 | } 33 | 34 | @Bean 35 | fun passwordEncoder(): PasswordEncoder = { password -> 36 | CRYPT_ENCODER.encode(password) 37 | } 38 | 39 | @Bean 40 | fun authenticationContext(): SetAuthentication = { 41 | SecurityContextHolder.getContext().authentication = it 42 | } 43 | 44 | @Bean(name = [(BeanIds.AUTHENTICATION_MANAGER)]) 45 | override fun authenticationManagerBean(): AuthenticationManager { 46 | return super.authenticationManagerBean() 47 | } 48 | 49 | @Autowired 50 | fun configureGlobal(auth: AuthenticationManagerBuilder) { 51 | auth.userDetailsService(userDetailsService) 52 | .passwordEncoder(CRYPT_ENCODER) 53 | } 54 | 55 | override fun configure(http: HttpSecurity) { 56 | http 57 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() 58 | .authorizeRequests() 59 | .antMatchers("/auth/**").permitAll() 60 | .antMatchers("/v2/api-docs").permitAll() 61 | .antMatchers("/swagger-resources/**").permitAll() 62 | .antMatchers("/swagger-ui.html**").permitAll() 63 | .antMatchers("/webjars/**").permitAll() 64 | .antMatchers("/h2-console/**").permitAll() 65 | .antMatchers("/actuator/**").permitAll() 66 | .anyRequest().authenticated() 67 | .and() 68 | .headers().frameOptions().sameOrigin() 69 | .and() 70 | .addFilterBefore( 71 | TokenAuthenticationFilter(tokenHelper, userDetailsService, authenticationContext()), 72 | UsernamePasswordAuthenticationFilter::class.java 73 | ) 74 | .csrf().disable() 75 | .formLogin().disable() 76 | .httpBasic().disable() 77 | .exceptionHandling() 78 | .authenticationEntryPoint(JwtAuthenticationEntryPoint()) 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/security/SecurityUser.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.security 2 | 3 | import com.karumi.springbootkotlin.developers.domain.Developer 4 | import com.karumi.springbootkotlin.developers.domain.DeveloperValidator 5 | import org.springframework.security.core.GrantedAuthority 6 | import org.springframework.security.core.userdetails.UserDetails 7 | 8 | class SecurityUser( 9 | val id: String, 10 | private val username: String, 11 | private val password: String, 12 | private val authorities: List = emptyList() 13 | ) : UserDetails { 14 | override fun getAuthorities(): Collection = authorities 15 | 16 | override fun isEnabled(): Boolean = true 17 | 18 | override fun getUsername(): String = username 19 | 20 | override fun isCredentialsNonExpired(): Boolean = true 21 | 22 | override fun getPassword(): String = password 23 | 24 | override fun isAccountNonExpired(): Boolean = true 25 | 26 | override fun isAccountNonLocked(): Boolean = true 27 | } 28 | 29 | fun Developer.toSecurityUser(): SecurityUser = SecurityUser( 30 | id.toString(), username, password, getRole() 31 | ) 32 | 33 | private fun Developer.getRole() = listOf( 34 | if (DeveloperValidator.isKarumiDeveloper(this)) { 35 | GrantedAuthority { "ROLE_KARUMIER" } 36 | } else { 37 | GrantedAuthority { "ROLE_USER" } 38 | } 39 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/security/TokenAuthenticationFilter.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.security 2 | 3 | import com.karumi.springbootkotlin.common.TryLogger 4 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken 5 | import org.springframework.web.filter.OncePerRequestFilter 6 | import javax.servlet.FilterChain 7 | import javax.servlet.http.HttpServletRequest 8 | import javax.servlet.http.HttpServletResponse 9 | 10 | class TokenAuthenticationFilter( 11 | private val tokenHelper: TokenHelper, 12 | private val userDetailsService: CustomUserDetailsService, 13 | private val setAuthentication: SetAuthentication 14 | ) : OncePerRequestFilter() { 15 | 16 | companion object { 17 | private const val AUTHORIZATION_HEADER = "Authorization" 18 | private const val BEARER = "Bearer " 19 | } 20 | 21 | override fun doFilterInternal( 22 | request: HttpServletRequest, 23 | response: HttpServletResponse, 24 | filterChain: FilterChain 25 | ) { 26 | TryLogger { 27 | val token = getJwtToken(request) 28 | 29 | if (token != null && tokenHelper.isValidToken(token)) { 30 | val username = tokenHelper.getUsernameFromToken(token) 31 | val userDetails = userDetailsService.loadUserByUsername(username) 32 | val authentication = 33 | UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities) 34 | setAuthentication(authentication) 35 | } 36 | } 37 | 38 | filterChain.doFilter(request, response) 39 | } 40 | 41 | private fun getJwtToken(request: HttpServletRequest): String? = 42 | request.getHeader(AUTHORIZATION_HEADER)?.let { token -> 43 | if (token.startsWith(BEARER)) { 44 | token.substring(7) 45 | } else { 46 | null 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/security/TokenHelper.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.security 2 | 3 | import com.karumi.springbootkotlin.common.TryLogger 4 | import io.jsonwebtoken.Jwts 5 | import io.jsonwebtoken.SignatureAlgorithm 6 | import org.springframework.stereotype.Component 7 | import java.util.Date 8 | import java.util.concurrent.TimeUnit 9 | 10 | @Component 11 | class 12 | TokenHelper { 13 | 14 | companion object { 15 | private const val SECRET_TOKEN = "*top*secret*key" 16 | private val SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512 17 | private val EXPIRES_IN_TEN_MINUTES = TimeUnit.MINUTES.toMillis(10) 18 | } 19 | 20 | fun generateToken(username: String): String = 21 | Jwts.builder() 22 | .setSubject(username) 23 | .setIssuedAt(now()) 24 | .setExpiration(getExpirationDate()) 25 | .signWith(SIGNATURE_ALGORITHM, SECRET_TOKEN) 26 | .compact() 27 | 28 | fun isValidToken(token: String): Boolean = TryLogger { 29 | Jwts.parser() 30 | .setSigningKey(SECRET_TOKEN) 31 | .parseClaimsJws(token) 32 | }.isSuccess() 33 | 34 | fun getUsernameFromToken(token: String): String = 35 | Jwts.parser() 36 | .setSigningKey(SECRET_TOKEN) 37 | .parseClaimsJws(token) 38 | .body 39 | .subject 40 | 41 | private fun now() = Date(System.currentTimeMillis()) 42 | private fun getExpirationDate() = Date(now().time + EXPIRES_IN_TEN_MINUTES) 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/karumi/springbootkotlin/workers/SimpleScheduler.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.workers 2 | 3 | import org.slf4j.LoggerFactory 4 | import org.springframework.scheduling.annotation.Scheduled 5 | import org.springframework.stereotype.Component 6 | import java.util.concurrent.CompletableFuture 7 | 8 | @Component 9 | class SimpleScheduler { 10 | 11 | companion object { 12 | private val logger = LoggerFactory.getLogger(SimpleScheduler::class.java) 13 | } 14 | 15 | @Scheduled(fixedRate = 1000) 16 | fun execute() { 17 | val startDate = System.currentTimeMillis() 18 | 19 | val task1 = hardWork() 20 | val task2 = hardWork() 21 | val task3 = hardWork() 22 | 23 | CompletableFuture.allOf(task1, task2, task3).join() 24 | 25 | val endDate = System.currentTimeMillis() 26 | logger.info("Total time: ${(endDate - startDate) / 1000}s") 27 | } 28 | 29 | fun hardWork(): CompletableFuture = 30 | CompletableFuture.supplyAsync { Thread.sleep(1000) } 31 | } -------------------------------------------------------------------------------- /src/main/requests/Actuator.http: -------------------------------------------------------------------------------- 1 | 2 | # For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). 3 | # 4 | # Following HTTP Request Live Templates are available: 5 | # * 'gtrp' and 'gtr' create a GET request with or without query parameters; 6 | # * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; 7 | # * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); 8 | 9 | GET http://{{host}}:{{port}}/actuator 10 | Accept: application/json 11 | Authorization: Bearer {{token}} 12 | 13 | ### 14 | 15 | GET http://{{host}}:{{port}}/actuator/info 16 | Accept: application/json 17 | Authorization: Bearer {{token}} 18 | 19 | ### 20 | 21 | GET http://{{host}}:{{port}}/actuator/health 22 | Accept: application/json 23 | Authorization: Bearer {{token}} 24 | 25 | ### 26 | 27 | GET http://{{host}}:{{port}}/actuator/metrics 28 | Accept: application/json 29 | Authorization: Bearer {{token}} 30 | 31 | ### 32 | 33 | GET http://{{host}}:{{port}}/actuator/metrics/jvm.memory.used 34 | Accept: application/json 35 | Authorization: Bearer {{token}} 36 | 37 | ### 38 | 39 | GET http://{{host}}:{{port}}/heapdump 40 | Accept: application/json 41 | Authorization: Bearer {{token}} 42 | 43 | ### 44 | 45 | GET http://{{host}}:{{port}}/httptrace 46 | Accept: application/json 47 | Authorization: Bearer {{token}} 48 | 49 | ### 50 | -------------------------------------------------------------------------------- /src/main/requests/Authentication.http: -------------------------------------------------------------------------------- 1 | POST http://{{host}}:{{port}}/auth/register 2 | Content-Type: application/json 3 | 4 | { 5 | "username": "example", 6 | "email": "example@karumi.com", 7 | "password": "password" 8 | } 9 | 10 | ### 11 | 12 | POST http://{{host}}:{{port}}/auth/login 13 | Content-Type: application/json 14 | 15 | { 16 | "username": "example", 17 | "password": "password" 18 | } 19 | 20 | > {% 21 | client.global.set("token", response.body) 22 | %} 23 | 24 | ### 25 | -------------------------------------------------------------------------------- /src/main/requests/DeveloperController.http: -------------------------------------------------------------------------------- 1 | POST http://{{host}}:{{port}}/developer 2 | Content-Type: application/json 3 | Authorization: Bearer {{token}} 4 | 5 | { 6 | "email": "toni@karumi.com", 7 | "username": "tonilopezmr", 8 | "password": "1234" 9 | } 10 | 11 | > {% 12 | client.global.set("devId", response.body.id) 13 | %} 14 | 15 | ### 16 | 17 | GET http://{{host}}:{{port}}/developer/{{devId}} 18 | Accept: application/json 19 | Authorization: Bearer {{token}} 20 | 21 | ### 22 | 23 | 24 | GET http://{{host}}:{{port}}/developer/ba166a3c-f9ce-414c-baad-1737038d49b2 25 | Accept: application/json 26 | Authorization: Bearer {{token}} 27 | 28 | ### 29 | -------------------------------------------------------------------------------- /src/main/requests/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "host": "localhost", 4 | "port": 5000 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=5000 2 | spring.boot.admin.client.url=http://localhost:8080 3 | spring.boot.admin.client.username=admin 4 | spring.boot.admin.client.password=admin-password 5 | spring.h2.console.enabled=true 6 | spring.h2.console.path=/h2 7 | management.endpoints.web.exposure.include=* 8 | management.endpoint.health.show-details=always 9 | spring.main.allow-bean-definition-overriding=true 10 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karumi/springbootkotlin/MockCustomUser.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin 2 | 3 | import com.karumi.springbootkotlin.security.SecurityUser 4 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken 5 | import org.springframework.security.core.context.SecurityContext 6 | import org.springframework.security.core.context.SecurityContextHolder 7 | import org.springframework.security.test.context.support.WithSecurityContext 8 | import org.springframework.security.test.context.support.WithSecurityContextFactory 9 | 10 | class WithMockCustomUserSecurityContextFactory : WithSecurityContextFactory { 11 | override fun createSecurityContext(customUser: WithMockCustomUser): SecurityContext { 12 | val context = SecurityContextHolder.createEmptyContext() 13 | 14 | val principal = SecurityUser("1234", customUser.name, customUser.username) 15 | val auth = UsernamePasswordAuthenticationToken(principal, "1234", principal.authorities) 16 | context.authentication = auth 17 | return context 18 | } 19 | } 20 | 21 | @Retention(value = AnnotationRetention.RUNTIME) 22 | @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory::class) 23 | annotation class WithMockCustomUser(val username: String = "Test", val name: String = "Sr. Test") -------------------------------------------------------------------------------- /src/test/kotlin/com/karumi/springbootkotlin/controllers/DeveloperControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.controllers 2 | 3 | import arrow.core.getOrElse 4 | import com.karumi.springbootkotlin.authentication.api.NewDeveloperRequest 5 | import com.karumi.springbootkotlin.developers.api.DeveloperBody 6 | import com.karumi.springbootkotlin.developers.domain.Developer 7 | import com.karumi.springbootkotlin.developers.storage.DeveloperDao 8 | import com.karumi.springbootkotlin.developers.storage.DeveloperRepository 9 | import com.karumi.springbootkotlin.getAuthorized 10 | import com.karumi.springbootkotlin.given.GivenDeveloper 11 | import com.karumi.springbootkotlin.postAuthorized 12 | import com.karumi.springbootkotlin.uuid 13 | import io.kotlintest.provided.CleanDatabaseListener 14 | import io.kotlintest.shouldBe 15 | import io.kotlintest.specs.StringSpec 16 | import io.kotlintest.spring.SpringListener 17 | import org.springframework.beans.factory.annotation.Autowired 18 | import org.springframework.boot.test.context.SpringBootTest 19 | import org.springframework.boot.test.web.client.TestRestTemplate 20 | import org.springframework.boot.web.server.LocalServerPort 21 | import org.springframework.http.HttpStatus.BAD_REQUEST 22 | import org.springframework.http.HttpStatus.CREATED 23 | import org.springframework.http.HttpStatus.NOT_FOUND 24 | import org.springframework.http.HttpStatus.OK 25 | import org.springframework.http.HttpStatus.UNAUTHORIZED 26 | import org.springframework.http.ResponseEntity 27 | import java.util.UUID 28 | 29 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 30 | class DeveloperControllerTest @Autowired constructor( 31 | private val dao: DeveloperDao, 32 | private val restTemplate: TestRestTemplate, 33 | private val repository: DeveloperRepository 34 | ) : StringSpec(), GivenDeveloper { 35 | 36 | @LocalServerPort 37 | var PORT: Int = 0 38 | 39 | val URL 40 | get() = "http://localhost:$PORT" 41 | 42 | override fun listeners() = listOf(SpringListener, CleanDatabaseListener(repository)) 43 | 44 | init { 45 | 46 | "developer POST should create a developer if it's a karumi developer" { 47 | val newDeveloper = NEW_KARUMI_DEVELOPER 48 | val result = withSession().postDeveloper(newDeveloper) 49 | val createdDeveloper = result.body!! 50 | val obtainedDeveloper = getById(createdDeveloper.id.uuid()) 51 | 52 | result.statusCode shouldBe CREATED 53 | createdDeveloper.username shouldBe newDeveloper.username 54 | createdDeveloper.email shouldBe newDeveloper.email 55 | obtainedDeveloper.username shouldBe newDeveloper.username 56 | obtainedDeveloper.email shouldBe newDeveloper.email 57 | } 58 | 59 | "developer POST should returns 400 if it isn't a karumi developer" { 60 | val result = withSession().post(ANY_NEW_DEVELOPER) 61 | 62 | result.statusCode shouldBe BAD_REQUEST 63 | } 64 | 65 | "developer POST should returns 400 if the json body isn't expected" { 66 | val result = withSession().post(InvalidModel()) 67 | 68 | result.statusCode shouldBe BAD_REQUEST 69 | } 70 | 71 | "developer POST should returns 401 if doesn't have authentication token" { 72 | val result = restTemplate.post(ANY_NEW_DEVELOPER) 73 | 74 | result.statusCode shouldBe UNAUTHORIZED 75 | } 76 | 77 | "developer GET should retrieve by id" { 78 | val developer = create(KARUMI_DEVELOPER) 79 | 80 | val result = withSession().getDeveloper(developer) 81 | 82 | result.statusCode shouldBe OK 83 | result.body?.username shouldBe developer.username 84 | result.body?.email shouldBe developer.email 85 | } 86 | 87 | "developer GET should returns 404 if there isn't the developer in the database" { 88 | val result = withSession().get(DEVELOPER_ID) 89 | 90 | result.statusCode shouldBe NOT_FOUND 91 | } 92 | 93 | "developer GET should returns 401 if doesn't have authentication token" { 94 | val result = restTemplate.get(DEVELOPER_ID) 95 | 96 | result.statusCode shouldBe UNAUTHORIZED 97 | } 98 | } 99 | 100 | private fun TestRestTemplate.postDeveloper( 101 | developer: NewDeveloperRequest 102 | ): ResponseEntity = postAuthorized("$URL/developer", developer) 103 | 104 | private fun TestRestTemplate.post(value: A): ResponseEntity = 105 | postAuthorized("$URL/developer", value) 106 | 107 | private fun TestRestTemplate.getDeveloper(developer: Developer): ResponseEntity = 108 | getAuthorized("$URL/developer/${developer.id}") 109 | 110 | private fun TestRestTemplate.get(id: UUID): ResponseEntity = 111 | getAuthorized("$URL/developer/$id") 112 | 113 | private data class InvalidModel(val invalid: String = "") 114 | 115 | private fun withSession(): TestRestTemplate { 116 | create(SESSION_DEVELOPER) 117 | return restTemplate 118 | } 119 | 120 | private fun getById(id: UUID) = dao.getById(id).getOrElse { null }?.getOrElse { null }!! 121 | private fun create(developer: Developer) = dao.create(developer).getOrElse { null }!! 122 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/karumi/springbootkotlin/controllers/DeveloperControllerTestMockUser.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.controllers 2 | 3 | import arrow.core.None 4 | import arrow.core.some 5 | import arrow.core.success 6 | import com.karumi.springbootkotlin.WithMockCustomUser 7 | import com.karumi.springbootkotlin.andExpectContent 8 | import com.karumi.springbootkotlin.developers.storage.DeveloperDao 9 | import com.karumi.springbootkotlin.given.GivenDeveloper 10 | import com.karumi.springbootkotlin.withContent 11 | import com.ninjasquad.springmockk.MockkBean 12 | import io.mockk.every 13 | import org.junit.jupiter.api.Test 14 | import org.springframework.beans.factory.annotation.Autowired 15 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 16 | import org.springframework.boot.test.context.SpringBootTest 17 | import org.springframework.http.MediaType 18 | import org.springframework.test.web.servlet.MockMvc 19 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 20 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post 21 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print 22 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers 23 | 24 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) 25 | @AutoConfigureMockMvc 26 | class DeveloperControllerTestMockUser( 27 | @Autowired private val mockMvc: MockMvc 28 | ) : GivenDeveloper { 29 | 30 | @MockkBean 31 | lateinit var developerDao: DeveloperDao 32 | 33 | @Test 34 | @WithMockCustomUser 35 | fun `developer POST should create a developer if it's a karumi developer`() { 36 | every { developerDao.create(any()) } returns KARUMI_DEVELOPER.success() 37 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 38 | 39 | mockMvc.perform( 40 | post("/developer") 41 | .contentType(MediaType.APPLICATION_JSON) 42 | .withContent("CreateKarumiDeveloper.json") 43 | ).andExpect(MockMvcResultMatchers.status().isCreated) 44 | .andExpectContent("ExpectedNewDeveloper.json") 45 | .andDo(print()) 46 | } 47 | 48 | @Test 49 | @WithMockCustomUser 50 | fun `developer POST should returns 400 if it isn't a karumi developer `() { 51 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 52 | 53 | mockMvc.perform( 54 | post("/developer") 55 | .contentType(MediaType.APPLICATION_JSON) 56 | .withContent("CreateDeveloper.json") 57 | ).andExpect(MockMvcResultMatchers.status().isBadRequest) 58 | .andDo(print()) 59 | } 60 | 61 | @Test 62 | @WithMockCustomUser 63 | fun `developer POST should returns 400 if the json body isn't expected`() { 64 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 65 | 66 | mockMvc.perform( 67 | post("/developer") 68 | .contentType(MediaType.APPLICATION_JSON) 69 | .withContent("BadDeveloperBody.json") 70 | ).andExpect(MockMvcResultMatchers.status().isBadRequest) 71 | .andDo(print()) 72 | } 73 | 74 | @Test 75 | fun `developer POST should returns 401 if doesn't have authentication token`() { 76 | every { developerDao.getByUsername(any()) } returns None.success() 77 | 78 | mockMvc.perform( 79 | post("/developer") 80 | .contentType(MediaType.APPLICATION_JSON) 81 | .withContent("CreateKarumiDeveloper.json") 82 | ).andExpect(MockMvcResultMatchers.status().isUnauthorized) 83 | .andDo(print()) 84 | } 85 | 86 | @Test 87 | @WithMockCustomUser 88 | fun `developer GET should retrieve by id`() { 89 | every { developerDao.getById(DEVELOPER_ID) } returns KARUMI_DEVELOPER.some().success() 90 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 91 | 92 | mockMvc.perform( 93 | get("/developer/$DEVELOPER_ID") 94 | .contentType(MediaType.APPLICATION_JSON) 95 | ).andExpect(MockMvcResultMatchers.status().isOk) 96 | .andExpectContent("ExpectedNewDeveloper.json") 97 | .andDo(print()) 98 | } 99 | 100 | @Test 101 | @WithMockCustomUser 102 | fun `developer GET should returns 404 if there isn't the developer in the database`() { 103 | every { developerDao.getById(DEVELOPER_ID) } returns None.success() 104 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 105 | 106 | mockMvc.perform( 107 | get("/developer/$DEVELOPER_ID") 108 | .contentType(MediaType.APPLICATION_JSON) 109 | ).andExpect(MockMvcResultMatchers.status().isNotFound) 110 | .andDo(print()) 111 | } 112 | 113 | @Test 114 | fun `developer GET should returns 401 if doesn't have authentication token`() { 115 | mockMvc.perform( 116 | get("/developer/$DEVELOPER_ID") 117 | .contentType(MediaType.APPLICATION_JSON) 118 | ).andExpect(MockMvcResultMatchers.status().isUnauthorized) 119 | .andDo(print()) 120 | } 121 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/karumi/springbootkotlin/controllers/DeveloperControllerUsingMvcMock.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.controllers 2 | 3 | import arrow.core.None 4 | import arrow.core.some 5 | import arrow.core.success 6 | import com.karumi.springbootkotlin.developers.storage.DeveloperDao 7 | import com.karumi.springbootkotlin.given.GivenDeveloper 8 | import com.karumi.springbootkotlin.matchWithSnapshot 9 | import com.karumi.springbootkotlin.withAuthorization 10 | import com.karumi.springbootkotlin.withContent 11 | import com.ninjasquad.springmockk.MockkBean 12 | import io.kotlintest.extensions.TestListener 13 | import io.kotlintest.specs.WordSpec 14 | import io.kotlintest.spring.SpringListener 15 | import io.mockk.every 16 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 17 | import org.springframework.boot.test.context.SpringBootTest 18 | import org.springframework.http.MediaType 19 | import org.springframework.test.web.servlet.MockMvc 20 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 21 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post 22 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print 23 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 24 | 25 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) 26 | @AutoConfigureMockMvc 27 | class DeveloperControllerUsingMvcMock( 28 | private val mockMvc: MockMvc, 29 | @MockkBean val developerDao: DeveloperDao 30 | ) : WordSpec(), GivenDeveloper { 31 | 32 | override fun listeners(): List = listOf(SpringListener) 33 | 34 | init { 35 | "POST /developer" should { 36 | "create a developer if it's a karumi developer" { 37 | every { developerDao.create(any()) } returns KARUMI_DEVELOPER.success() 38 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 39 | 40 | mockMvc.perform( 41 | post("/developer") 42 | .contentType(MediaType.APPLICATION_JSON) 43 | .withContent("CreateKarumiDeveloper.json") 44 | .withAuthorization() 45 | ).andExpect(status().isCreated) 46 | .andDo(print()) 47 | .matchWithSnapshot(this) 48 | } 49 | 50 | "returns 400 if it isn't a karumi developer" { 51 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 52 | 53 | mockMvc.perform( 54 | post("/developer") 55 | .contentType(MediaType.APPLICATION_JSON) 56 | .withContent("CreateDeveloper.json") 57 | .withAuthorization() 58 | ).andExpect(status().isBadRequest) 59 | .andDo(print()) 60 | } 61 | 62 | "returns 400 if the json body isn't expected" { 63 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 64 | 65 | mockMvc.perform( 66 | post("/developer") 67 | .contentType(MediaType.APPLICATION_JSON) 68 | .withContent("BadDeveloperBody.json") 69 | .withAuthorization() 70 | ).andExpect(status().isBadRequest) 71 | .andDo(print()) 72 | } 73 | 74 | "developer POST should returns 401 if doesn't have authentication token" { 75 | every { developerDao.getByUsername(any()) } returns None.success() 76 | 77 | mockMvc.perform( 78 | post("/developer") 79 | .contentType(MediaType.APPLICATION_JSON) 80 | .withContent("CreateKarumiDeveloper.json") 81 | ).andExpect(status().isUnauthorized) 82 | .andDo(print()) 83 | } 84 | } 85 | 86 | "GET /developer/{id}" should { 87 | "retrieve by id" { 88 | every { developerDao.getById(DEVELOPER_ID) } returns KARUMI_DEVELOPER.some().success() 89 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 90 | 91 | mockMvc.perform( 92 | get("/developer/$DEVELOPER_ID") 93 | .contentType(MediaType.APPLICATION_JSON) 94 | .withAuthorization() 95 | ).andExpect(status().isOk) 96 | .andDo(print()) 97 | .matchWithSnapshot(this) 98 | } 99 | 100 | "returns 404 code if there isn't the developer in the database" { 101 | every { developerDao.getById(DEVELOPER_ID) } returns None.success() 102 | every { developerDao.getByUsername(any()) } returns SESSION_DEVELOPER.some().success() 103 | 104 | mockMvc.perform( 105 | get("/developer/$DEVELOPER_ID") 106 | .contentType(MediaType.APPLICATION_JSON) 107 | .withAuthorization() 108 | ).andExpect(status().isNotFound) 109 | .andDo(print()) 110 | } 111 | 112 | "developer GET should returns 401 if doesn't have authentication token" { 113 | mockMvc.perform( 114 | get("/developer/$DEVELOPER_ID") 115 | .contentType(MediaType.APPLICATION_JSON) 116 | ).andExpect(status().isUnauthorized) 117 | .andDo(print()) 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/karumi/springbootkotlin/developers/storage/DeveloperRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.developers.storage 2 | 3 | import com.karumi.springbootkotlin.developers.domain.Developer 4 | import com.karumi.springbootkotlin.given.GivenDeveloper 5 | import io.kotlintest.shouldBe 6 | import io.kotlintest.specs.StringSpec 7 | import io.kotlintest.spring.SpringListener 8 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest 9 | import org.springframework.transaction.annotation.Transactional 10 | import java.util.UUID 11 | 12 | @DataJpaTest 13 | @Transactional 14 | class DeveloperRepositoryTest( 15 | val repository: DeveloperRepository 16 | ) : StringSpec(), GivenDeveloper { 17 | 18 | override fun listeners() = listOf(SpringListener) 19 | 20 | init { 21 | "developer should be updated" { 22 | val developer = save(KARUMI_DEVELOPER) 23 | 24 | val developerUpdate = developer.copy(username = "Pedro") 25 | val updatedDeveloper = repository.save(developerUpdate) 26 | 27 | updatedDeveloper shouldBe developerUpdate 28 | find(developer.id) shouldBe developerUpdate 29 | } 30 | } 31 | 32 | private fun save(developer: Developer) = 33 | repository.save(developer.toEntity()) 34 | 35 | private fun find(id: UUID?) = 36 | repository.findById(id!!).get() 37 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/karumi/springbootkotlin/developers/storage/kotlintestconfig.kt: -------------------------------------------------------------------------------- 1 | package io.kotlintest.provided 2 | 3 | import com.karumi.springbootkotlin.developers.storage.DeveloperRepository 4 | import io.kotlintest.AbstractProjectConfig 5 | import io.kotlintest.TestCase 6 | import io.kotlintest.extensions.ProjectLevelExtension 7 | import io.kotlintest.extensions.TestListener 8 | import io.kotlintest.spring.SpringAutowireConstructorExtension 9 | 10 | class ProjectConfig : AbstractProjectConfig() { 11 | override fun extensions(): List = 12 | listOf(SpringAutowireConstructorExtension) 13 | } 14 | 15 | class CleanDatabaseListener(private val developerRepository: DeveloperRepository) : TestListener { 16 | override fun beforeTest(testCase: TestCase) { 17 | developerRepository.deleteAll() 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/karumi/springbootkotlin/extensions.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin 2 | 3 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 4 | import com.karumi.kotlinsnapshot.matchWithSnapshot 5 | import com.karumi.springbootkotlin.developers.api.DeveloperBody 6 | import com.karumi.springbootkotlin.security.TokenHelper 7 | import io.kotlintest.TestContext 8 | import org.springframework.boot.test.web.client.TestRestTemplate 9 | import org.springframework.core.io.ClassPathResource 10 | import org.springframework.http.HttpEntity 11 | import org.springframework.http.HttpHeaders 12 | import org.springframework.http.HttpHeaders.AUTHORIZATION 13 | import org.springframework.http.HttpMethod 14 | import org.springframework.http.HttpMethod.GET 15 | import org.springframework.http.HttpMethod.POST 16 | import org.springframework.http.ResponseEntity 17 | import org.springframework.test.web.servlet.ResultActions 18 | import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder 19 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 20 | import java.nio.file.Files 21 | import java.util.UUID 22 | 23 | inline fun TestRestTemplate.postAuthorized(url: String, body: B): ResponseEntity = 24 | withAuthorization(url, POST, body) 25 | 26 | inline fun TestRestTemplate.getAuthorized(url: String): ResponseEntity = 27 | withAuthorization(url, GET) 28 | 29 | inline fun TestRestTemplate.withAuthorization( 30 | url: String, 31 | method: HttpMethod, 32 | body: B? = null 33 | ): ResponseEntity { 34 | val headers = HttpHeaders() 35 | headers.set(AUTHORIZATION, "Bearer ${getTokenForTestUser()}") 36 | val entity = HttpEntity(body, headers) 37 | return this.exchange(url, method, entity, A::class.java) 38 | } 39 | 40 | fun MockHttpServletRequestBuilder.withAuthorization(): MockHttpServletRequestBuilder = 41 | header(AUTHORIZATION, "Bearer ${getTokenForTestUser()}") 42 | 43 | fun ResultActions.andExpectContent(fileName: String): ResultActions = 44 | andExpect(content().json(getContent(fileName))) 45 | 46 | fun MockHttpServletRequestBuilder.withContent(fileName: String): MockHttpServletRequestBuilder { 47 | val content = getContent(fileName) 48 | content(content) 49 | return this 50 | } 51 | 52 | private fun getContent(fileName: String): String { 53 | val resource = ClassPathResource(fileName).file 54 | return String(Files.readAllBytes(resource.toPath())) 55 | } 56 | 57 | fun getTokenForTestUser(): String = 58 | TokenHelper().generateToken("Test") 59 | 60 | fun ResultActions.getDeveloper(): DeveloperBody { 61 | val mapper = jacksonObjectMapper() 62 | return mapper.readValue(andReturn().response.contentAsString, DeveloperBody::class.java) 63 | } 64 | 65 | fun ResultActions.matchWithSnapshot(context: TestContext? = null) { 66 | andReturn().response.contentAsString.matchWithSnapshot(context?.description()?.name) 67 | } 68 | 69 | fun ResponseEntity<*>.matchWithSnapshot(context: TestContext) { 70 | this.body!!.matchWithSnapshot(context.description().name) 71 | } 72 | 73 | fun String.uuid(): UUID = UUID.fromString(this) -------------------------------------------------------------------------------- /src/test/kotlin/com/karumi/springbootkotlin/given/GivenDeveloper.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.springbootkotlin.given 2 | 3 | import com.karumi.springbootkotlin.authentication.api.NewDeveloperRequest 4 | import com.karumi.springbootkotlin.developers.domain.Developer 5 | import com.karumi.springbootkotlin.uuid 6 | 7 | interface GivenDeveloper { 8 | val DEVELOPER_ID 9 | get() = "c04ecc99-d7ba-460c-be78-6995805c5175".uuid() 10 | val SESSION_DEVELOPER 11 | get() = Developer( 12 | id = "c04ecc99-d7ba-460c-be78-6995805c5177".uuid(), 13 | username = "Test", 14 | email = "test@karumi.com", 15 | password = "1234" 16 | ) 17 | val NEW_KARUMI_DEVELOPER 18 | get() = NewDeveloperRequest( 19 | username = "Unknown", 20 | email = "email@karumi.com", 21 | password = "1234" 22 | ) 23 | val KARUMI_DEVELOPER 24 | get() = Developer( 25 | id = DEVELOPER_ID, 26 | username = "Unknown", 27 | email = "email@karumi.com", 28 | password = "1234" 29 | ) 30 | 31 | val ANY_NEW_DEVELOPER 32 | get() = NewDeveloperRequest( 33 | username = "Unknown", 34 | email = "email@email.com", 35 | password = "1234" 36 | ) 37 | } -------------------------------------------------------------------------------- /src/test/resources/BadDeveloperBody.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/test/resources/CreateDeveloper.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "email@email.com", 3 | "username": "Unknown", 4 | "password": "1234" 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/CreateKarumiDeveloper.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "email@karumi.com", 3 | "username": "Unknown", 4 | "password": "1234" 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/ExpectedDeveloper.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "c04ecc99-d7ba-460c-be78-6995805c5175", 3 | "username": "Unknown", 4 | "email": "email@karumi.com" 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/ExpectedNewDeveloper.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "c04ecc99-d7ba-460c-be78-6995805c5175", 3 | "username": "Unknown", 4 | "email": "email@karumi.com" 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/LoginRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "Unknown", 3 | "password": "1234" 4 | } 5 | -------------------------------------------------------------------------------- /travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | before_install: 5 | - chmod +x gradlew 6 | - chmod +x gradle/wrapper/gradle-wrapper.jar 7 | script: 8 | - ./gradlew ktlint test 9 | --------------------------------------------------------------------------------