├── .github └── workflows │ ├── Build.yml │ └── Deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── config └── detekt │ ├── baseline.xml │ └── detekt.yml ├── doc └── postman │ └── MobileAPI.postman_collection.json ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── cvillaseca │ │ └── mobileapi │ │ ├── MobileApiApplication.kt │ │ ├── config │ │ ├── database │ │ │ └── DatabaseConfig │ │ └── security │ │ │ ├── AuthServerConfig.kt │ │ │ ├── ResourceServerConfig.kt │ │ │ └── WebSecurityConfig.kt │ │ ├── controller │ │ ├── admin │ │ │ └── UserController.kt │ │ └── user │ │ │ ├── ChangePasswordController.kt │ │ │ ├── MeController.kt │ │ │ └── SignUpController.kt │ │ ├── dao │ │ ├── RoleDao.kt │ │ ├── UserDao.kt │ │ └── UserInfoDao.kt │ │ ├── model │ │ ├── Role.kt │ │ ├── User.kt │ │ └── UserInfo.kt │ │ └── service │ │ └── UserService.kt └── resources │ ├── application.properties │ ├── data.sql │ └── schema.sql └── test └── kotlin └── com └── cvillaseca └── mobileapi ├── MobileApiApplicationTests.kt ├── controller ├── admin │ └── UserControllerTest.kt └── user │ ├── ChangePasswordControllerTest.kt │ ├── MeControllerTest.kt │ └── SignUpControllerTest.kt └── service └── UserServiceTest.kt /.github/workflows/Build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | detekt: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up JDK 1.8 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 1.8 16 | 17 | - uses: actions/cache@v2 18 | with: 19 | path: ~/.gradle 20 | key: ${{ runner.os }}-gradle-${{ hashFiles('build.gradle.kts') }}-${{ hashFiles('buildSrc/src/main/java/Dependencies.kt') }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} 21 | restore-keys: | 22 | ${{ runner.os }}-gradle- 23 | 24 | - name: Run detekt 25 | run: ./gradlew detekt 26 | 27 | tests: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | 33 | - name: Set up JDK 1.8 34 | uses: actions/setup-java@v1 35 | with: 36 | java-version: 1.8 37 | 38 | - uses: actions/cache@v2 39 | with: 40 | path: ~/.gradle 41 | key: ${{ runner.os }}-gradle-${{ hashFiles('build.gradle.kts') }}-${{ hashFiles('buildSrc/src/main/java/Dependencies.kt') }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} 42 | restore-keys: | 43 | ${{ runner.os }}-gradle- 44 | 45 | - name: Run tests 46 | run: ./gradlew test 47 | -------------------------------------------------------------------------------- /.github/workflows/Deploy.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Deploy to Heroku 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: akhileshns/heroku-deploy@v3.0.4 15 | with: 16 | heroku_api_key: ${{secrets.HEROKU_API_KEY}} 17 | heroku_app_name: ${{secrets.HEROKU_APP_NAME}} 18 | heroku_email: ${{secrets.HEROKU_EMAIL}} 19 | -------------------------------------------------------------------------------- /.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 | #IntelliJ files 23 | **/*.iml 24 | **/.idea 25 | **/.gradle 26 | 27 | # Generated files 28 | bin/ 29 | gen/ 30 | out/ 31 | 32 | # Gradle files 33 | .gradle 34 | !gradle-wrapper.jar 35 | /build/ 36 | 37 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 38 | hs_err_pid* 39 | -------------------------------------------------------------------------------- /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 | # mobileAPI 2 | REST API with OAuth2 using Springboot 2.2.X written in Kotlin 3 | 4 | This repository is part of a series of tutorials: 5 | - [Part 1. First Controller](https://proandroiddev.com/how-to-create-a-rest-api-for-your-app-with-spring-boot-kotlin-gradle-part-1-first-controller-c19fe075e968) 6 | - [Part 2. Securing with OAuth2](https://proandroiddev.com/how-to-create-a-rest-api-for-your-app-with-spring-boot-kotlin-gradle-part-2-security-with-32f944918fe1) 7 | - [Part 3. Adding a H2 database](https://proandroiddev.com/how-to-create-a-rest-api-for-your-app-with-spring-boot-kotlin-gradle-part-3-adding-a-h2-7f9e6219b367) 8 | - [Part 4. Testing the API](https://proandroiddev.com/how-to-create-a-rest-api-for-your-app-with-spring-boot-kotlin-gradle-part-4-testing-a66ab6846e8f) 9 | - [Part 5. Deploy on Heroku](https://proandroiddev.com/how-to-create-a-rest-api-for-your-app-with-spring-boot-kotlin-gradle-part-5-deploy-on-heroku-ff21e77ea5f3) 10 | 11 | ### Instructions 12 | 13 | 1 - Run the server 14 | ``` 15 | ./gradlew bootRun 16 | ``` 17 | 18 | 2 - Test the API 19 | 20 | There is a [postman configuration](./doc/postman/MobileAPI.postman_collection.json) 21 | that you can import and start testing the web services. 22 | 23 | ### Gradle tasks 24 | ``` 25 | ./gradlew detekt //Code analysis. 26 | ./gradlew checkDependencyUpdates //Check dependency updates. 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("org.springframework.boot") version "2.3.1.RELEASE" 5 | id("io.spring.dependency-management") version "1.0.9.RELEASE" 6 | kotlin("jvm") version "1.3.72" 7 | kotlin("plugin.spring") version "1.3.72" 8 | 9 | id("io.gitlab.arturbosch.detekt").version("1.10.0") 10 | id("name.remal.check-dependency-updates").version("1.0.199") 11 | } 12 | 13 | group = "com.cvillaseca" 14 | version = "0.0.1-SNAPSHOT" 15 | java.sourceCompatibility = JavaVersion.VERSION_1_8 16 | 17 | repositories { 18 | mavenCentral() 19 | jcenter() 20 | } 21 | 22 | dependencies { 23 | implementation("org.jetbrains.kotlin:kotlin-reflect") 24 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 25 | 26 | implementation("org.springframework.boot:spring-boot-starter") 27 | implementation("org.springframework.boot:spring-boot-starter-web") 28 | implementation("org.springframework.boot:spring-boot-starter-jdbc") 29 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 30 | implementation("org.springframework.security.oauth:spring-security-oauth2:2.5.0.RELEASE") 31 | 32 | runtimeOnly("com.h2database:h2") 33 | 34 | testImplementation("org.springframework.boot:spring-boot-starter-test") { 35 | exclude(group = "org.junit.vintage", module = "junit-vintage-engine") 36 | exclude(module = "mockito-core") 37 | } 38 | testImplementation("org.springframework.security:spring-security-test") 39 | testImplementation("com.ninja-squad:springmockk:2.0.2") 40 | 41 | detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.10.0") 42 | } 43 | 44 | tasks.withType { 45 | useJUnitPlatform() 46 | } 47 | 48 | tasks.withType { 49 | kotlinOptions { 50 | freeCompilerArgs = listOf("-Xjsr305=strict") 51 | jvmTarget = "1.8" 52 | } 53 | } 54 | 55 | detekt { 56 | toolVersion = "1.9.1" 57 | config = files("${rootProject.projectDir}/config/detekt/detekt.yml") 58 | baseline = file("${rootProject.projectDir}/config/detekt/baseline.xml") 59 | buildUponDefaultConfig = true 60 | } 61 | -------------------------------------------------------------------------------- /config/detekt/baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SpreadOperator:MobileApiApplication.kt$(*args) 6 | 7 | 8 | -------------------------------------------------------------------------------- /config/detekt/detekt.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | excludeCorrectable: false 4 | weights: 5 | # complexity: 2 6 | # LongParameterList: 1 7 | # style: 1 8 | # comments: 1 9 | 10 | config: 11 | validation: true 12 | # when writing own rules with new properties, exclude the property path e.g.: "my_rule_set,.*>.*>[my_property]" 13 | excludes: "" 14 | 15 | processors: 16 | active: true 17 | exclude: 18 | - 'DetektProgressListener' 19 | # - 'FunctionCountProcessor' 20 | # - 'PropertyCountProcessor' 21 | # - 'ClassCountProcessor' 22 | # - 'PackageCountProcessor' 23 | # - 'KtFileCountProcessor' 24 | 25 | console-reports: 26 | active: true 27 | exclude: 28 | - 'ProjectStatisticsReport' 29 | - 'ComplexityReport' 30 | - 'NotificationReport' 31 | # - 'FindingsReport' 32 | - 'FileBasedFindingsReport' 33 | 34 | comments: 35 | active: true 36 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 37 | CommentOverPrivateFunction: 38 | active: false 39 | CommentOverPrivateProperty: 40 | active: false 41 | EndOfSentenceFormat: 42 | active: false 43 | endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$) 44 | UndocumentedPublicClass: 45 | active: false 46 | searchInNestedClass: true 47 | searchInInnerClass: true 48 | searchInInnerObject: true 49 | searchInInnerInterface: true 50 | UndocumentedPublicFunction: 51 | active: false 52 | UndocumentedPublicProperty: 53 | active: false 54 | 55 | complexity: 56 | active: true 57 | ComplexCondition: 58 | active: true 59 | threshold: 4 60 | ComplexInterface: 61 | active: false 62 | threshold: 10 63 | includeStaticDeclarations: false 64 | ComplexMethod: 65 | active: true 66 | threshold: 15 67 | ignoreSingleWhenExpression: false 68 | ignoreSimpleWhenEntries: false 69 | ignoreNestingFunctions: false 70 | nestingFunctions: run,let,apply,with,also,use,forEach,isNotNull,ifNull 71 | LabeledExpression: 72 | active: false 73 | ignoredLabels: "" 74 | LargeClass: 75 | active: true 76 | threshold: 600 77 | LongMethod: 78 | active: true 79 | threshold: 60 80 | LongParameterList: 81 | active: true 82 | threshold: 6 83 | ignoreDefaultParameters: false 84 | MethodOverloading: 85 | active: false 86 | threshold: 6 87 | NestedBlockDepth: 88 | active: true 89 | threshold: 4 90 | StringLiteralDuplication: 91 | active: false 92 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 93 | threshold: 3 94 | ignoreAnnotation: true 95 | excludeStringsWithLessThan5Characters: true 96 | ignoreStringsRegex: '$^' 97 | TooManyFunctions: 98 | active: true 99 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 100 | thresholdInFiles: 30 101 | thresholdInClasses: 30 102 | thresholdInInterfaces: 30 103 | thresholdInObjects: 30 104 | thresholdInEnums: 30 105 | ignoreDeprecated: false 106 | ignorePrivate: false 107 | ignoreOverridden: false 108 | 109 | coroutines: 110 | active: true 111 | GlobalCoroutineUsage: 112 | active: false 113 | RedundantSuspendModifier: 114 | active: false 115 | 116 | empty-blocks: 117 | active: true 118 | EmptyCatchBlock: 119 | active: true 120 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 121 | EmptyClassBlock: 122 | active: true 123 | EmptyDefaultConstructor: 124 | active: true 125 | EmptyDoWhileBlock: 126 | active: true 127 | EmptyElseBlock: 128 | active: true 129 | EmptyFinallyBlock: 130 | active: true 131 | EmptyForBlock: 132 | active: true 133 | EmptyFunctionBlock: 134 | active: true 135 | ignoreOverridden: true 136 | EmptyIfBlock: 137 | active: true 138 | EmptyInitBlock: 139 | active: true 140 | EmptyKtFile: 141 | active: true 142 | EmptySecondaryConstructor: 143 | active: true 144 | EmptyWhenBlock: 145 | active: true 146 | EmptyWhileBlock: 147 | active: true 148 | 149 | exceptions: 150 | active: true 151 | ExceptionRaisedInUnexpectedLocation: 152 | active: false 153 | methodNames: 'toString,hashCode,equals,finalize' 154 | InstanceOfCheckForException: 155 | active: false 156 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 157 | NotImplementedDeclaration: 158 | active: false 159 | PrintStackTrace: 160 | active: false 161 | RethrowCaughtException: 162 | active: false 163 | ReturnFromFinally: 164 | active: false 165 | ignoreLabeled: false 166 | SwallowedException: 167 | active: false 168 | ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' 169 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 170 | ThrowingExceptionFromFinally: 171 | active: false 172 | ThrowingExceptionInMain: 173 | active: false 174 | ThrowingExceptionsWithoutMessageOrCause: 175 | active: false 176 | exceptions: 'IllegalArgumentException,IllegalStateException,IOException' 177 | ThrowingNewInstanceOfSameException: 178 | active: false 179 | TooGenericExceptionCaught: 180 | active: true 181 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 182 | exceptionNames: 183 | - ArrayIndexOutOfBoundsException 184 | - Error 185 | - Exception 186 | - IllegalMonitorStateException 187 | - NullPointerException 188 | - IndexOutOfBoundsException 189 | - RuntimeException 190 | - Throwable 191 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 192 | TooGenericExceptionThrown: 193 | active: true 194 | exceptionNames: 195 | - Error 196 | - Exception 197 | - Throwable 198 | - RuntimeException 199 | 200 | formatting: 201 | active: true 202 | android: false 203 | autoCorrect: true 204 | AnnotationOnSeparateLine: 205 | active: false 206 | autoCorrect: true 207 | ChainWrapping: 208 | active: true 209 | autoCorrect: true 210 | CommentSpacing: 211 | active: true 212 | autoCorrect: true 213 | EnumEntryNameCase: 214 | active: false 215 | autoCorrect: true 216 | Filename: 217 | active: true 218 | FinalNewline: 219 | active: true 220 | autoCorrect: true 221 | ImportOrdering: 222 | active: false 223 | autoCorrect: true 224 | Indentation: 225 | active: false 226 | autoCorrect: true 227 | indentSize: 4 228 | continuationIndentSize: 4 229 | MaximumLineLength: 230 | active: true 231 | maxLineLength: 120 232 | ModifierOrdering: 233 | active: true 234 | autoCorrect: true 235 | MultiLineIfElse: 236 | active: true 237 | autoCorrect: true 238 | NoBlankLineBeforeRbrace: 239 | active: true 240 | autoCorrect: true 241 | NoConsecutiveBlankLines: 242 | active: true 243 | autoCorrect: true 244 | NoEmptyClassBody: 245 | active: true 246 | autoCorrect: true 247 | NoEmptyFirstLineInMethodBlock: 248 | active: false 249 | autoCorrect: true 250 | NoLineBreakAfterElse: 251 | active: true 252 | autoCorrect: true 253 | NoLineBreakBeforeAssignment: 254 | active: true 255 | autoCorrect: true 256 | NoMultipleSpaces: 257 | active: true 258 | autoCorrect: true 259 | NoSemicolons: 260 | active: true 261 | autoCorrect: true 262 | NoTrailingSpaces: 263 | active: true 264 | autoCorrect: true 265 | NoUnitReturn: 266 | active: true 267 | autoCorrect: true 268 | NoUnusedImports: 269 | active: true 270 | autoCorrect: true 271 | NoWildcardImports: 272 | active: true 273 | PackageName: 274 | active: true 275 | autoCorrect: true 276 | ParameterListWrapping: 277 | active: true 278 | autoCorrect: true 279 | indentSize: 4 280 | SpacingAroundColon: 281 | active: true 282 | autoCorrect: true 283 | SpacingAroundComma: 284 | active: true 285 | autoCorrect: true 286 | SpacingAroundCurly: 287 | active: true 288 | autoCorrect: true 289 | SpacingAroundDot: 290 | active: true 291 | autoCorrect: true 292 | SpacingAroundKeyword: 293 | active: true 294 | autoCorrect: true 295 | SpacingAroundOperators: 296 | active: true 297 | autoCorrect: true 298 | SpacingAroundParens: 299 | active: true 300 | autoCorrect: true 301 | SpacingAroundRangeOperator: 302 | active: true 303 | autoCorrect: true 304 | StringTemplate: 305 | active: true 306 | autoCorrect: true 307 | 308 | naming: 309 | active: true 310 | ClassNaming: 311 | active: true 312 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 313 | classPattern: '[A-Z$][a-zA-Z0-9$]*' 314 | ConstructorParameterNaming: 315 | active: true 316 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 317 | parameterPattern: '[a-z][A-Za-z0-9]*' 318 | privateParameterPattern: '[a-z][A-Za-z0-9]*' 319 | excludeClassPattern: '$^' 320 | ignoreOverridden: true 321 | EnumNaming: 322 | active: true 323 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 324 | enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' 325 | ForbiddenClassName: 326 | active: false 327 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 328 | forbiddenName: '' 329 | FunctionMaxLength: 330 | active: false 331 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 332 | maximumFunctionNameLength: 30 333 | FunctionMinLength: 334 | active: false 335 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 336 | minimumFunctionNameLength: 3 337 | FunctionNaming: 338 | active: true 339 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 340 | functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' 341 | excludeClassPattern: '$^' 342 | ignoreOverridden: true 343 | FunctionParameterNaming: 344 | active: true 345 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 346 | parameterPattern: '[a-z][A-Za-z0-9]*' 347 | excludeClassPattern: '$^' 348 | ignoreOverridden: true 349 | InvalidPackageDeclaration: 350 | active: false 351 | rootPackage: '' 352 | MatchingDeclarationName: 353 | active: true 354 | MemberNameEqualsClassName: 355 | active: true 356 | ignoreOverridden: true 357 | ObjectPropertyNaming: 358 | active: true 359 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 360 | constantPattern: '[A-Za-z][_A-Za-z0-9]*' 361 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 362 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' 363 | PackageNaming: 364 | active: true 365 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 366 | packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' 367 | TopLevelPropertyNaming: 368 | active: true 369 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 370 | constantPattern: '[A-Z][_A-Z0-9]*' 371 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 372 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' 373 | VariableMaxLength: 374 | active: false 375 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 376 | maximumVariableNameLength: 64 377 | VariableMinLength: 378 | active: false 379 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 380 | minimumVariableNameLength: 1 381 | VariableNaming: 382 | active: true 383 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 384 | variablePattern: '[a-z][A-Za-z0-9]*' 385 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' 386 | excludeClassPattern: '$^' 387 | ignoreOverridden: true 388 | 389 | performance: 390 | active: true 391 | ArrayPrimitive: 392 | active: true 393 | ForEachOnRange: 394 | active: true 395 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 396 | SpreadOperator: 397 | active: true 398 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 399 | UnnecessaryTemporaryInstantiation: 400 | active: true 401 | 402 | potential-bugs: 403 | active: true 404 | Deprecation: 405 | active: false 406 | DuplicateCaseInWhenExpression: 407 | active: true 408 | EqualsAlwaysReturnsTrueOrFalse: 409 | active: true 410 | EqualsWithHashCodeExist: 411 | active: true 412 | ExplicitGarbageCollectionCall: 413 | active: true 414 | HasPlatformType: 415 | active: false 416 | ImplicitDefaultLocale: 417 | active: false 418 | InvalidRange: 419 | active: true 420 | IteratorHasNextCallsNextMethod: 421 | active: true 422 | IteratorNotThrowingNoSuchElementException: 423 | active: true 424 | LateinitUsage: 425 | active: false 426 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 427 | excludeAnnotatedProperties: "" 428 | ignoreOnClassesPattern: "" 429 | MapGetWithNotNullAssertionOperator: 430 | active: false 431 | MissingWhenCase: 432 | active: true 433 | RedundantElseInWhen: 434 | active: true 435 | UnconditionalJumpStatementInLoop: 436 | active: false 437 | UnreachableCode: 438 | active: true 439 | UnsafeCallOnNullableType: 440 | active: true 441 | UnsafeCast: 442 | active: false 443 | UselessPostfixExpression: 444 | active: false 445 | WrongEqualsTypeParameter: 446 | active: true 447 | 448 | style: 449 | active: true 450 | CollapsibleIfStatements: 451 | active: false 452 | DataClassContainsFunctions: 453 | active: false 454 | conversionFunctionPrefix: 'to' 455 | DataClassShouldBeImmutable: 456 | active: false 457 | EqualsNullCall: 458 | active: true 459 | EqualsOnSignatureLine: 460 | active: false 461 | ExplicitCollectionElementAccessMethod: 462 | active: false 463 | ExplicitItLambdaParameter: 464 | active: false 465 | ExpressionBodySyntax: 466 | active: false 467 | includeLineWrapping: false 468 | ForbiddenComment: 469 | active: true 470 | values: 'TODO:,FIXME:,STOPSHIP:' 471 | allowedPatterns: "" 472 | ForbiddenImport: 473 | active: false 474 | imports: '' 475 | forbiddenPatterns: "" 476 | ForbiddenMethodCall: 477 | active: false 478 | methods: '' 479 | ForbiddenPublicDataClass: 480 | active: false 481 | ignorePackages: '*.internal,*.internal.*' 482 | ForbiddenVoid: 483 | active: false 484 | ignoreOverridden: false 485 | ignoreUsageInGenerics: false 486 | FunctionOnlyReturningConstant: 487 | active: true 488 | ignoreOverridableFunction: true 489 | excludedFunctions: 'describeContents' 490 | excludeAnnotatedFunction: 491 | LibraryCodeMustSpecifyReturnType: 492 | active: true 493 | LoopWithTooManyJumpStatements: 494 | active: true 495 | maxJumpCount: 1 496 | MagicNumber: 497 | active: true 498 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 499 | ignoreNumbers: '-1,0,1,2' 500 | ignoreHashCodeFunction: true 501 | ignorePropertyDeclaration: false 502 | ignoreLocalVariableDeclaration: false 503 | ignoreConstantDeclaration: true 504 | ignoreCompanionObjectPropertyDeclaration: true 505 | ignoreAnnotation: false 506 | ignoreNamedArgument: true 507 | ignoreEnums: false 508 | ignoreRanges: false 509 | MandatoryBracesIfStatements: 510 | active: false 511 | MaxLineLength: 512 | active: true 513 | maxLineLength: 120 514 | excludePackageStatements: true 515 | excludeImportStatements: true 516 | excludeCommentStatements: false 517 | MayBeConst: 518 | active: true 519 | ModifierOrder: 520 | active: true 521 | NestedClassesVisibility: 522 | active: false 523 | NewLineAtEndOfFile: 524 | active: true 525 | NoTabs: 526 | active: false 527 | OptionalAbstractKeyword: 528 | active: true 529 | OptionalUnit: 530 | active: false 531 | OptionalWhenBraces: 532 | active: false 533 | PreferToOverPairSyntax: 534 | active: false 535 | ProtectedMemberInFinalClass: 536 | active: true 537 | RedundantExplicitType: 538 | active: false 539 | RedundantVisibilityModifierRule: 540 | active: false 541 | ReturnCount: 542 | active: true 543 | max: 2 544 | excludedFunctions: "equals" 545 | excludeLabeled: false 546 | excludeReturnFromLambda: true 547 | excludeGuardClauses: false 548 | SafeCast: 549 | active: true 550 | SerialVersionUIDInSerializableClass: 551 | active: false 552 | SpacingBetweenPackageAndImports: 553 | active: false 554 | ThrowsCount: 555 | active: true 556 | max: 2 557 | TrailingWhitespace: 558 | active: false 559 | UnderscoresInNumericLiterals: 560 | active: false 561 | acceptableDecimalLength: 5 562 | UnnecessaryAbstractClass: 563 | active: true 564 | excludeAnnotatedClasses: 565 | UnnecessaryAnnotationUseSiteTarget: 566 | active: false 567 | UnnecessaryApply: 568 | active: false 569 | UnnecessaryInheritance: 570 | active: true 571 | UnnecessaryLet: 572 | active: false 573 | UnnecessaryParentheses: 574 | active: false 575 | UntilInsteadOfRangeTo: 576 | active: false 577 | UnusedImports: 578 | active: false 579 | UnusedPrivateClass: 580 | active: true 581 | UnusedPrivateMember: 582 | active: false 583 | allowedNames: "(_|ignored|expected|serialVersionUID)" 584 | UseArrayLiteralsInAnnotations: 585 | active: false 586 | UseCheckOrError: 587 | active: false 588 | UseDataClass: 589 | active: false 590 | excludeAnnotatedClasses: "" 591 | allowVars: false 592 | UseIfInsteadOfWhen: 593 | active: false 594 | UseRequire: 595 | active: false 596 | UselessCallOnNotNull: 597 | active: true 598 | UtilityClassWithPublicConstructor: 599 | active: true 600 | VarCouldBeVal: 601 | active: false 602 | WildcardImport: 603 | active: true 604 | excludes: "**/test/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 605 | excludeImports: 'java.util.*' -------------------------------------------------------------------------------- /doc/postman/MobileAPI.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "dbabc9b7-33d9-4999-ac16-9f3fdf770cd1", 4 | "name": "MobileAPI", 5 | "description": "Postman client for our MobileAPI", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 7 | }, 8 | "item": [ 9 | { 10 | "name": "oauth", 11 | "item": [ 12 | { 13 | "name": "/oauth/token client credentials", 14 | "event": [ 15 | { 16 | "listen": "test", 17 | "script": { 18 | "id": "1d448cd0-d204-4f13-978e-590859e96a15", 19 | "exec": [ 20 | "var jsonData = JSON.parse(responseBody);", 21 | "", 22 | "pm.globals.set(\"access_token\", jsonData.access_token);", 23 | "" 24 | ], 25 | "type": "text/javascript" 26 | } 27 | } 28 | ], 29 | "request": { 30 | "auth": { 31 | "type": "basic", 32 | "basic": [ 33 | { 34 | "key": "password", 35 | "value": "secret_android", 36 | "type": "string" 37 | }, 38 | { 39 | "key": "username", 40 | "value": "client_android", 41 | "type": "string" 42 | }, 43 | { 44 | "key": "saveHelperData", 45 | "type": "any" 46 | }, 47 | { 48 | "key": "showPassword", 49 | "value": false, 50 | "type": "boolean" 51 | } 52 | ] 53 | }, 54 | "method": "POST", 55 | "header": [], 56 | "body": { 57 | "mode": "formdata", 58 | "formdata": [ 59 | { 60 | "key": "grant_type", 61 | "value": "client_credentials", 62 | "type": "text" 63 | }, 64 | { 65 | "key": "username", 66 | "value": "admin", 67 | "type": "text", 68 | "disabled": true 69 | }, 70 | { 71 | "key": "password", 72 | "value": "1234", 73 | "type": "text", 74 | "disabled": true 75 | } 76 | ], 77 | "options": { 78 | "formdata": {} 79 | } 80 | }, 81 | "url": { 82 | "raw": "http://localhost:8080/mobileAPI/oauth/token", 83 | "protocol": "http", 84 | "host": [ 85 | "localhost" 86 | ], 87 | "port": "8080", 88 | "path": [ 89 | "mobileAPI", 90 | "oauth", 91 | "token" 92 | ] 93 | } 94 | }, 95 | "response": [] 96 | }, 97 | { 98 | "name": "/oauth/token password", 99 | "event": [ 100 | { 101 | "listen": "test", 102 | "script": { 103 | "id": "28d55a72-db68-4f83-87e4-badcf09a776d", 104 | "exec": [ 105 | "var jsonData = JSON.parse(responseBody);", 106 | "", 107 | "pm.globals.set(\"access_token\", jsonData.access_token);", 108 | "" 109 | ], 110 | "type": "text/javascript" 111 | } 112 | } 113 | ], 114 | "request": { 115 | "auth": { 116 | "type": "basic", 117 | "basic": [ 118 | { 119 | "key": "password", 120 | "value": "secret_android", 121 | "type": "string" 122 | }, 123 | { 124 | "key": "username", 125 | "value": "client_android", 126 | "type": "string" 127 | }, 128 | { 129 | "key": "saveHelperData", 130 | "type": "any" 131 | }, 132 | { 133 | "key": "showPassword", 134 | "value": false, 135 | "type": "boolean" 136 | } 137 | ] 138 | }, 139 | "method": "POST", 140 | "header": [], 141 | "body": { 142 | "mode": "formdata", 143 | "formdata": [ 144 | { 145 | "key": "grant_type", 146 | "value": "password", 147 | "type": "text" 148 | }, 149 | { 150 | "key": "username", 151 | "value": "user@example.com", 152 | "type": "text" 153 | }, 154 | { 155 | "key": "password", 156 | "value": "1234", 157 | "type": "text" 158 | } 159 | ], 160 | "options": { 161 | "formdata": {} 162 | } 163 | }, 164 | "url": { 165 | "raw": "http://localhost:8080/mobileAPI/oauth/token", 166 | "protocol": "http", 167 | "host": [ 168 | "localhost" 169 | ], 170 | "port": "8080", 171 | "path": [ 172 | "mobileAPI", 173 | "oauth", 174 | "token" 175 | ] 176 | } 177 | }, 178 | "response": [] 179 | }, 180 | { 181 | "name": "/oauth/token refresh token", 182 | "event": [ 183 | { 184 | "listen": "test", 185 | "script": { 186 | "id": "70df314d-aeb6-48e4-a1fb-9b3baebbccc5", 187 | "exec": [ 188 | "var jsonData = JSON.parse(responseBody);", 189 | "", 190 | "pm.globals.set(\"access_token\", jsonData.access_token);", 191 | "" 192 | ], 193 | "type": "text/javascript" 194 | } 195 | } 196 | ], 197 | "request": { 198 | "auth": { 199 | "type": "basic", 200 | "basic": [ 201 | { 202 | "key": "password", 203 | "value": "secret_android", 204 | "type": "string" 205 | }, 206 | { 207 | "key": "username", 208 | "value": "client_android", 209 | "type": "string" 210 | }, 211 | { 212 | "key": "saveHelperData", 213 | "type": "any" 214 | }, 215 | { 216 | "key": "showPassword", 217 | "value": false, 218 | "type": "boolean" 219 | } 220 | ] 221 | }, 222 | "method": "POST", 223 | "header": [], 224 | "body": { 225 | "mode": "formdata", 226 | "formdata": [ 227 | { 228 | "key": "grant_type", 229 | "value": "refresh_token", 230 | "type": "text" 231 | }, 232 | { 233 | "key": "refresh_token", 234 | "value": "f72fc22d-aef9-4f9e-ae45-4e59e7067f73", 235 | "type": "text" 236 | }, 237 | { 238 | "key": "", 239 | "value": "", 240 | "type": "text", 241 | "disabled": true 242 | } 243 | ], 244 | "options": { 245 | "formdata": {} 246 | } 247 | }, 248 | "url": { 249 | "raw": "http://localhost:8080/mobileAPI/oauth/token", 250 | "protocol": "http", 251 | "host": [ 252 | "localhost" 253 | ], 254 | "port": "8080", 255 | "path": [ 256 | "mobileAPI", 257 | "oauth", 258 | "token" 259 | ] 260 | } 261 | }, 262 | "response": [] 263 | } 264 | ], 265 | "protocolProfileBehavior": {} 266 | }, 267 | { 268 | "name": "private", 269 | "item": [ 270 | { 271 | "name": "/changePassword", 272 | "request": { 273 | "auth": { 274 | "type": "oauth2", 275 | "oauth2": [ 276 | { 277 | "key": "accessToken", 278 | "value": "{{access_token}}", 279 | "type": "string" 280 | }, 281 | { 282 | "key": "addTokenTo", 283 | "value": "header", 284 | "type": "string" 285 | }, 286 | { 287 | "key": "callBackUrl", 288 | "type": "any" 289 | }, 290 | { 291 | "key": "authUrl", 292 | "type": "any" 293 | }, 294 | { 295 | "key": "accessTokenUrl", 296 | "type": "any" 297 | }, 298 | { 299 | "key": "clientId", 300 | "type": "any" 301 | }, 302 | { 303 | "key": "clientSecret", 304 | "type": "any" 305 | }, 306 | { 307 | "key": "clientAuth", 308 | "type": "any" 309 | }, 310 | { 311 | "key": "grantType", 312 | "type": "any" 313 | }, 314 | { 315 | "key": "scope", 316 | "type": "any" 317 | }, 318 | { 319 | "key": "username", 320 | "type": "any" 321 | }, 322 | { 323 | "key": "password", 324 | "type": "any" 325 | }, 326 | { 327 | "key": "tokenType", 328 | "value": "bearer", 329 | "type": "string" 330 | }, 331 | { 332 | "key": "redirectUri", 333 | "type": "any" 334 | }, 335 | { 336 | "key": "refreshToken", 337 | "type": "any" 338 | } 339 | ] 340 | }, 341 | "method": "POST", 342 | "header": [], 343 | "body": { 344 | "mode": "formdata", 345 | "formdata": [ 346 | { 347 | "key": "newPassword", 348 | "value": "12345", 349 | "type": "text" 350 | }, 351 | { 352 | "key": "oldPassword", 353 | "value": "1234", 354 | "type": "text" 355 | } 356 | ], 357 | "options": { 358 | "formdata": {} 359 | } 360 | }, 361 | "url": { 362 | "raw": "http://localhost:8080/mobileAPI/user/changePassword", 363 | "protocol": "http", 364 | "host": [ 365 | "localhost" 366 | ], 367 | "port": "8080", 368 | "path": [ 369 | "mobileAPI", 370 | "user", 371 | "changePassword" 372 | ] 373 | } 374 | }, 375 | "response": [] 376 | }, 377 | { 378 | "name": "/me", 379 | "protocolProfileBehavior": { 380 | "disableBodyPruning": true 381 | }, 382 | "request": { 383 | "auth": { 384 | "type": "oauth2", 385 | "oauth2": [ 386 | { 387 | "key": "accessToken", 388 | "value": "{{access_token}}", 389 | "type": "string" 390 | }, 391 | { 392 | "key": "addTokenTo", 393 | "value": "header", 394 | "type": "string" 395 | } 396 | ] 397 | }, 398 | "method": "GET", 399 | "header": [], 400 | "body": { 401 | "mode": "formdata", 402 | "formdata": [] 403 | }, 404 | "url": { 405 | "raw": "http://localhost:8080/mobileAPI/user/me", 406 | "protocol": "http", 407 | "host": [ 408 | "localhost" 409 | ], 410 | "port": "8080", 411 | "path": [ 412 | "mobileAPI", 413 | "user", 414 | "me" 415 | ] 416 | } 417 | }, 418 | "response": [] 419 | } 420 | ], 421 | "protocolProfileBehavior": {} 422 | }, 423 | { 424 | "name": "admin", 425 | "item": [ 426 | { 427 | "name": "/admin/users", 428 | "protocolProfileBehavior": { 429 | "disableBodyPruning": true 430 | }, 431 | "request": { 432 | "auth": { 433 | "type": "oauth2", 434 | "oauth2": [ 435 | { 436 | "key": "accessToken", 437 | "value": "{{access_token}}", 438 | "type": "string" 439 | }, 440 | { 441 | "key": "addTokenTo", 442 | "value": "header", 443 | "type": "string" 444 | } 445 | ] 446 | }, 447 | "method": "GET", 448 | "header": [], 449 | "body": { 450 | "mode": "formdata", 451 | "formdata": [] 452 | }, 453 | "url": { 454 | "raw": "http://localhost:8080/mobileAPI/admin/users", 455 | "protocol": "http", 456 | "host": [ 457 | "localhost" 458 | ], 459 | "port": "8080", 460 | "path": [ 461 | "mobileAPI", 462 | "admin", 463 | "users" 464 | ] 465 | } 466 | }, 467 | "response": [] 468 | } 469 | ], 470 | "protocolProfileBehavior": {} 471 | }, 472 | { 473 | "name": "public", 474 | "item": [ 475 | { 476 | "name": "/signUp", 477 | "request": { 478 | "auth": { 479 | "type": "oauth2", 480 | "oauth2": [ 481 | { 482 | "key": "accessToken", 483 | "value": "{{access_token}}", 484 | "type": "string" 485 | }, 486 | { 487 | "key": "addTokenTo", 488 | "value": "header", 489 | "type": "string" 490 | } 491 | ] 492 | }, 493 | "method": "POST", 494 | "header": [], 495 | "body": { 496 | "mode": "formdata", 497 | "formdata": [ 498 | { 499 | "key": "email", 500 | "value": "newuser@example.com", 501 | "type": "text" 502 | }, 503 | { 504 | "key": "password", 505 | "value": "12345", 506 | "type": "text" 507 | } 508 | ] 509 | }, 510 | "url": { 511 | "raw": "http://localhost:8080/mobileAPI/signUp", 512 | "protocol": "http", 513 | "host": [ 514 | "localhost" 515 | ], 516 | "port": "8080", 517 | "path": [ 518 | "mobileAPI", 519 | "signUp" 520 | ] 521 | } 522 | }, 523 | "response": [] 524 | } 525 | ], 526 | "protocolProfileBehavior": {} 527 | } 528 | ], 529 | "protocolProfileBehavior": {} 530 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cvillaseca/mobileAPI/4b2fe4c652848418ed7ccf3b4e33303118cf46b7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "MobileAPI" 2 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/MobileApiApplication.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class MobileApiApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/config/database/DatabaseConfig: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.config.database 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.jdbc.datasource.DriverManagerDataSource 7 | import javax.sql.DataSource 8 | 9 | @Configuration 10 | class DatabaseConfig { 11 | 12 | @Value("\${spring.datasource.url}") 13 | private lateinit var datasourceUrl: String 14 | 15 | @Value("\${spring.datasource.driver-class-name}") 16 | private lateinit var dbDriverClassName: String 17 | 18 | @Value("\${spring.datasource.username}") 19 | private lateinit var dbUsername: String 20 | 21 | @Value("\${spring.datasource.password}") 22 | private lateinit var dbPassword: String 23 | 24 | @Bean 25 | fun dataSource(): DataSource = 26 | DriverManagerDataSource().apply { 27 | setDriverClassName(dbDriverClassName) 28 | url = datasourceUrl 29 | username = dbUsername 30 | password = dbPassword 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/config/security/AuthServerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.config.security 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.security.authentication.AuthenticationManager 7 | import org.springframework.security.core.userdetails.UserDetailsService 8 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter 10 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer 11 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer 12 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer 13 | import org.springframework.security.oauth2.provider.token.TokenStore 14 | import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore 15 | import javax.sql.DataSource 16 | 17 | @Configuration 18 | @EnableAuthorizationServer 19 | class AuthServerConfig : AuthorizationServerConfigurerAdapter() { 20 | 21 | @Autowired 22 | private lateinit var authenticationManager: AuthenticationManager 23 | 24 | @Autowired 25 | private lateinit var datasource: DataSource 26 | 27 | @Autowired 28 | private lateinit var userDetailsService: UserDetailsService 29 | 30 | @Bean 31 | fun tokenStore(): TokenStore = InMemoryTokenStore() 32 | 33 | @Throws(Exception::class) 34 | override fun configure(oauthServer: AuthorizationServerSecurityConfigurer) { 35 | oauthServer.checkTokenAccess("isAuthenticated()") 36 | } 37 | 38 | @Throws(Exception::class) 39 | override fun configure(clients: ClientDetailsServiceConfigurer) { 40 | clients.jdbc(datasource) 41 | } 42 | 43 | @Throws(Exception::class) 44 | override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) { 45 | endpoints.tokenStore(tokenStore()) 46 | .userDetailsService(userDetailsService) 47 | .authenticationManager(authenticationManager) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/config/security/ResourceServerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.config.security 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 5 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer 6 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter 7 | 8 | @Configuration 9 | @EnableResourceServer 10 | class ResourceServerConfig : ResourceServerConfigurerAdapter() { 11 | override fun configure(http: HttpSecurity) { 12 | http 13 | .csrf().disable() 14 | .formLogin().disable() 15 | .anonymous().disable() 16 | .authorizeRequests() 17 | .antMatchers("/admin/**").hasRole("ADMIN") 18 | .antMatchers("/user/**").hasRole("USER") 19 | .antMatchers("/signUp").authenticated() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/config/security/WebSecurityConfig.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.config.security 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.security.authentication.AuthenticationManager 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 10 | import org.springframework.security.core.userdetails.UserDetailsService 11 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 12 | 13 | @Configuration 14 | @EnableWebSecurity 15 | internal class WebSecurityConfig : WebSecurityConfigurerAdapter() { 16 | 17 | @Autowired 18 | private lateinit var userDetailsService: UserDetailsService 19 | 20 | @Bean 21 | fun passwordEncoder(): BCryptPasswordEncoder = BCryptPasswordEncoder() 22 | 23 | @Bean 24 | @Throws(Exception::class) 25 | override fun authenticationManagerBean(): AuthenticationManager { 26 | return super.authenticationManagerBean() 27 | } 28 | 29 | @Throws(Exception::class) 30 | override fun configure(auth: AuthenticationManagerBuilder) { 31 | auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/controller/admin/UserController.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.controller.admin 2 | 3 | import com.cvillaseca.mobileapi.model.UserInfo 4 | import com.cvillaseca.mobileapi.service.UserService 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.http.ResponseEntity 7 | import org.springframework.web.bind.annotation.RequestMapping 8 | import org.springframework.web.bind.annotation.RequestMethod 9 | import org.springframework.web.bind.annotation.RestController 10 | import javax.servlet.http.HttpServletRequest 11 | 12 | @RestController 13 | @RequestMapping("/admin") 14 | class UserController { 15 | @Autowired 16 | lateinit var userService: UserService 17 | 18 | @RequestMapping(value = ["/users"], method = [(RequestMethod.GET)]) 19 | fun getAllUsersInfo(request: HttpServletRequest): ResponseEntity> = 20 | ResponseEntity.ok(userService.getAllUsers()) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/controller/user/ChangePasswordController.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.controller.user 2 | 3 | import com.cvillaseca.mobileapi.service.UserService 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import org.springframework.http.ResponseEntity 6 | import org.springframework.web.bind.annotation.RequestMapping 7 | import org.springframework.web.bind.annotation.RequestMethod 8 | import org.springframework.web.bind.annotation.RequestParam 9 | import org.springframework.web.bind.annotation.RestController 10 | import javax.servlet.http.HttpServletRequest 11 | 12 | @RestController 13 | @RequestMapping("/user") 14 | class ChangePasswordController { 15 | 16 | @Autowired 17 | lateinit var userService: UserService 18 | 19 | @RequestMapping(value = ["/changePassword"], method = [(RequestMethod.POST)]) 20 | fun changePassword( 21 | @RequestParam("newPassword") newPassword: String, 22 | @RequestParam("oldPassword") oldPassword: String, 23 | request: HttpServletRequest 24 | ): ResponseEntity = 25 | if (userService.updatePassword(request.userPrincipal.name, oldPassword, newPassword)) { 26 | ResponseEntity.noContent().build() 27 | } else { 28 | ResponseEntity.badRequest().build() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/controller/user/MeController.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.controller.user 2 | 3 | import com.cvillaseca.mobileapi.model.UserInfo 4 | import com.cvillaseca.mobileapi.service.UserService 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.http.ResponseEntity 7 | import org.springframework.web.bind.annotation.RequestMapping 8 | import org.springframework.web.bind.annotation.RequestMethod 9 | import org.springframework.web.bind.annotation.RestController 10 | import javax.servlet.http.HttpServletRequest 11 | 12 | @RestController 13 | @RequestMapping("/user") 14 | class MeController { 15 | @Autowired 16 | lateinit var userService: UserService 17 | 18 | @RequestMapping(value = ["/me"], method = [(RequestMethod.GET)]) 19 | fun userInfo(request: HttpServletRequest): ResponseEntity = 20 | userService.getUserInfo(request.userPrincipal.name)?.let { 21 | ResponseEntity.ok(it) 22 | } ?: ResponseEntity.notFound().build() 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/controller/user/SignUpController.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.controller.user 2 | 3 | import com.cvillaseca.mobileapi.service.UserService 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import org.springframework.http.HttpStatus 6 | import org.springframework.http.ResponseEntity 7 | import org.springframework.web.bind.annotation.RequestMapping 8 | import org.springframework.web.bind.annotation.RequestMethod 9 | import org.springframework.web.bind.annotation.RequestParam 10 | import org.springframework.web.bind.annotation.RestController 11 | 12 | @RestController 13 | class SignUpController { 14 | 15 | @Autowired 16 | lateinit var userService: UserService 17 | 18 | @RequestMapping(value = ["/signUp"], method = [(RequestMethod.POST)]) 19 | fun addUser( 20 | @RequestParam email: String, 21 | @RequestParam password: String 22 | ): ResponseEntity = 23 | userService.createUser(email, password)?.let { 24 | ResponseEntity(it.id, HttpStatus.CREATED) 25 | } ?: ResponseEntity.status(HttpStatus.CONFLICT).build() 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/dao/RoleDao.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.dao 2 | 3 | import com.cvillaseca.mobileapi.model.Role 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | interface RoleDao : CrudRepository { 9 | fun findByName(name: String): Role 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/dao/UserDao.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.dao 2 | 3 | import com.cvillaseca.mobileapi.model.User 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | interface UserDao : CrudRepository { 9 | fun findOneByUsername(email: String): User? 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/dao/UserInfoDao.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.dao 2 | 3 | import com.cvillaseca.mobileapi.model.UserInfo 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | interface UserInfoDao : CrudRepository { 9 | fun findOneByUserId(userId: Long): UserInfo? 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/model/Role.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.model 2 | 3 | import java.io.Serializable 4 | import javax.persistence.Column 5 | import javax.persistence.Entity 6 | import javax.persistence.GeneratedValue 7 | import javax.persistence.GenerationType 8 | import javax.persistence.Id 9 | import javax.persistence.JoinColumn 10 | import javax.persistence.JoinTable 11 | import javax.persistence.ManyToMany 12 | import javax.persistence.Table 13 | 14 | @Entity 15 | @Table(name = "role") 16 | class Role( 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "role_id", nullable = false, updatable = false) 20 | val id: Long = 0, 21 | 22 | @Column(name = "role_name", nullable = false, unique = true) 23 | var name: String = "", 24 | 25 | @ManyToMany 26 | @JoinTable(name = "role_user", 27 | joinColumns = [JoinColumn(name = "role_id", referencedColumnName = "role_id")], 28 | inverseJoinColumns = [JoinColumn(name = "user_id", referencedColumnName = "user_id")]) 29 | val users: Set = HashSet() 30 | ) : Serializable 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.model 2 | 3 | import com.fasterxml.jackson.annotation.JsonBackReference 4 | import org.springframework.security.core.GrantedAuthority 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority 6 | import org.springframework.security.core.userdetails.UserDetails 7 | import java.util.ArrayList 8 | import javax.persistence.CascadeType 9 | import javax.persistence.Column 10 | import javax.persistence.Entity 11 | import javax.persistence.FetchType 12 | import javax.persistence.GeneratedValue 13 | import javax.persistence.GenerationType 14 | import javax.persistence.Id 15 | import javax.persistence.JoinColumn 16 | import javax.persistence.JoinTable 17 | import javax.persistence.ManyToMany 18 | import javax.persistence.Table 19 | 20 | @Entity 21 | @Table(name = "user") 22 | class User : UserDetails { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | @Column(name = "user_id", nullable = false, updatable = false) 27 | val id: Long = 0 28 | 29 | @Column(name = "username", nullable = false, unique = true) 30 | private var username: String = "" 31 | 32 | @Column(name = "password", nullable = false) 33 | private var password: String = "" 34 | 35 | @Column(name = "enabled", nullable = false) 36 | private var enabled: Boolean = true 37 | 38 | @ManyToMany(fetch = FetchType.EAGER, cascade = [(CascadeType.MERGE)]) 39 | @JsonBackReference 40 | @JoinTable(name = "role_user", 41 | joinColumns = [(JoinColumn(name = "user_id", referencedColumnName = "user_id"))], 42 | inverseJoinColumns = [(JoinColumn(name = "role_id", referencedColumnName = "role_id"))]) 43 | private var roles: Collection = emptyList() 44 | 45 | @Transient 46 | private val rolePrefix = "ROLE_" 47 | 48 | override fun getAuthorities(): MutableCollection { 49 | val list = ArrayList() 50 | 51 | for (role in roles) { 52 | list.add(SimpleGrantedAuthority(rolePrefix + role.name)) 53 | } 54 | 55 | return list 56 | } 57 | 58 | override fun isEnabled(): Boolean { 59 | return enabled 60 | } 61 | 62 | override fun getUsername(): String { 63 | return username 64 | } 65 | 66 | override fun getPassword(): String { 67 | return password 68 | } 69 | 70 | override fun isCredentialsNonExpired(): Boolean { 71 | return true 72 | } 73 | 74 | override fun isAccountNonExpired(): Boolean { 75 | return true 76 | } 77 | 78 | override fun isAccountNonLocked(): Boolean { 79 | return true 80 | } 81 | 82 | fun setRoles(roles: Collection) { 83 | this.roles = roles 84 | } 85 | 86 | fun setUsername(username: String) { 87 | this.username = username 88 | } 89 | 90 | fun setPassword(password: String) { 91 | this.password = password 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/model/UserInfo.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.model 2 | 3 | import javax.persistence.Column 4 | import javax.persistence.Entity 5 | import javax.persistence.GeneratedValue 6 | import javax.persistence.GenerationType 7 | import javax.persistence.Id 8 | import javax.persistence.Table 9 | 10 | @Entity 11 | @Table(name = "user_info") 12 | data class UserInfo( 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.IDENTITY) 15 | @Column(name = "user_info_id", nullable = false, updatable = false) 16 | private val id: Long = 0, 17 | 18 | @Column(name = "user_id", nullable = false, updatable = false) 19 | val userId: Long = 0, 20 | 21 | @Column(name = "email", nullable = false, unique = true) 22 | var email: String = "", 23 | 24 | @Column(name = "profile_image", nullable = true) 25 | var profileImage: String? = null 26 | ) 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/cvillaseca/mobileapi/service/UserService.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.service 2 | 3 | import com.cvillaseca.mobileapi.dao.RoleDao 4 | import com.cvillaseca.mobileapi.dao.UserDao 5 | import com.cvillaseca.mobileapi.dao.UserInfoDao 6 | import com.cvillaseca.mobileapi.model.User 7 | import com.cvillaseca.mobileapi.model.UserInfo 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.security.core.userdetails.UserDetails 10 | import org.springframework.security.core.userdetails.UserDetailsService 11 | import org.springframework.security.core.userdetails.UsernameNotFoundException 12 | import org.springframework.security.crypto.password.PasswordEncoder 13 | import org.springframework.stereotype.Service 14 | 15 | @Service("userDetailsService") 16 | class UserService : UserDetailsService { 17 | 18 | @Autowired 19 | private lateinit var userDao: UserDao 20 | 21 | @Autowired 22 | private lateinit var roleDao: RoleDao 23 | 24 | @Autowired 25 | private lateinit var userInfoDao: UserInfoDao 26 | 27 | @Autowired 28 | private lateinit var passwordEncoder: PasswordEncoder 29 | 30 | @Throws(UsernameNotFoundException::class) 31 | override fun loadUserByUsername(username: String): UserDetails = 32 | userDao.findOneByUsername(username) 33 | ?: throw UsernameNotFoundException("user not found") 34 | 35 | fun createUser(email: String, password: String): User? = 36 | if (userDao.findOneByUsername(email) == null) { 37 | val newUser = User() 38 | newUser.username = email 39 | newUser.password = passwordEncoder.encode(password) 40 | newUser.setRoles(listOf(roleDao.findByName("USER"))) 41 | val userSaved = userDao.save(newUser) 42 | userInfoDao.save( 43 | UserInfo( 44 | userId = userSaved.id, 45 | email = email 46 | ) 47 | ) 48 | userSaved 49 | } else null 50 | 51 | fun updatePassword(email: String, oldPassword: String, newPassword: String): Boolean { 52 | val user = userDao.findOneByUsername(email)!! 53 | return if (passwordEncoder.matches(oldPassword, user.password)) { 54 | user.password = passwordEncoder.encode(newPassword) 55 | userDao.save(user) 56 | true 57 | } else { 58 | false 59 | } 60 | } 61 | 62 | fun getUserInfo(email: String): UserInfo? = 63 | userDao.findOneByUsername(email)?.let { 64 | userInfoDao.findOneByUserId(it.id) 65 | } 66 | 67 | fun getAllUsers(): List = 68 | userInfoDao.findAll().toList() 69 | } 70 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | # =============================== 3 | # = SERVER CONFIG 4 | # =============================== 5 | 6 | server.port = 8080 7 | server.servlet.context-path=/mobileAPI 8 | 9 | # =============================== 10 | # = DATA SOURCE 11 | # =============================== 12 | 13 | spring.datasource.url=jdbc:h2:file:~/mobileDB 14 | spring.datasource.driverClassName=org.h2.Driver 15 | spring.datasource.username=sa 16 | spring.datasource.password=password 17 | spring.datasource.schema=classpath:/schema.sql 18 | spring.datasource.data=classpath:/data.sql 19 | 20 | # =============================== 21 | # = JPA 22 | # =============================== 23 | 24 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 25 | spring.jpa.hibernate.ddl-auto=validate 26 | 27 | ##Turn Statistics on 28 | spring.jpa.properties.hibernate.generate_statistics=true 29 | logging.level.org.hibernate.stat=debug 30 | 31 | ### Show or not log for each sql query 32 | spring.jpa.show-sql=true 33 | spring.jpa.properties.hibernate.format_sql=true 34 | logging.level.org.hibernate.type=trace -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO oauth_client_details 2 | (client_id, client_secret, scope, authorized_grant_types, 3 | web_server_redirect_uri, authorities, access_token_validity, 4 | refresh_token_validity, additional_information, autoapprove) 5 | VALUES 6 | ('client_android', '$2a$04$rRIQoDbmYiQf9QlKHJ3BXu30vgFv1QcLVJoKq9335mA7BKwiFplv6', 'foo,read,write','password,client_credentials,refresh_token', null, null, 36000, 36000, null, true), 7 | ('client_ios', '$2a$04$AnYUYnPT66TLY5fRh29FaO6Bj7bZf9g5HTlHdMF0cODpYgV7eliie', 'foo,read,write','password,client_credentials,refresh_token', null, null, 36000, 36000, null, true); 8 | 9 | INSERT INTO user (user_id, username, password, enabled) 10 | VALUES ('1', 'admin@example.com', '$2a$04$uBjpP8uyZ9I1SFhuT8L2ousq8V8OVla0AoooWDOXh589A7zfNG6QS', true), 11 | ('2', 'user@example.com', '$2a$04$uBjpP8uyZ9I1SFhuT8L2ousq8V8OVla0AoooWDOXh589A7zfNG6QS', true); 12 | 13 | INSERT INTO role (role_id, role_name) 14 | VALUES ('1', 'ADMIN'), 15 | ('2', 'USER'); 16 | 17 | INSERT INTO role_user (role_id, user_id) 18 | VALUES ('1', '1'), 19 | ('2', '2'); 20 | 21 | INSERT INTO user_info (user_info_id, user_id, email, profile_image) 22 | VALUES ('1', '1', 'admin@example.com', 'https://randomuser.me/api/portraits/women/4.jpg'), 23 | ('2', '2', 'user@example.com', 'https://randomuser.me/api/portraits/men/40.jpg'); -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS role_user; 2 | DROP TABLE IF EXISTS role; 3 | DROP TABLE IF EXISTS user_info; 4 | DROP TABLE IF EXISTS user; 5 | DROP TABLE IF EXISTS oauth_client_details; 6 | 7 | create table oauth_client_details 8 | ( 9 | client_id VARCHAR(256) PRIMARY KEY, 10 | resource_ids VARCHAR(256), 11 | client_secret VARCHAR(256), 12 | scope VARCHAR(256), 13 | authorized_grant_types VARCHAR(256), 14 | web_server_redirect_uri VARCHAR(256), 15 | authorities VARCHAR(256), 16 | access_token_validity INTEGER, 17 | refresh_token_validity INTEGER, 18 | additional_information VARCHAR(4096), 19 | autoapprove VARCHAR(256) 20 | ); 21 | 22 | CREATE TABLE user 23 | ( 24 | user_id BIGINT PRIMARY KEY auto_increment, 25 | username VARCHAR(128) UNIQUE, 26 | password VARCHAR(256), 27 | enabled BOOL, 28 | role BIGINT 29 | ); 30 | 31 | CREATE TABLE role 32 | ( 33 | role_id BIGINT PRIMARY KEY auto_increment, 34 | role_name VARCHAR(50) UNIQUE 35 | ); 36 | 37 | CREATE TABLE role_user 38 | ( 39 | id BIGINT PRIMARY KEY auto_increment, 40 | user_id BIGINT, 41 | role_id BIGINT 42 | ); 43 | 44 | CREATE TABLE user_info 45 | ( 46 | user_info_id BIGINT PRIMARY KEY auto_increment, 47 | user_id BIGINT UNIQUE REFERENCES user (user_id), 48 | email VARCHAR(128) UNIQUE, 49 | profile_image VARCHAR(256) 50 | ); -------------------------------------------------------------------------------- /src/test/kotlin/com/cvillaseca/mobileapi/MobileApiApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | 6 | @SpringBootTest 7 | class MobileApiApplicationTests { 8 | 9 | @Test 10 | fun contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/kotlin/com/cvillaseca/mobileapi/controller/admin/UserControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.controller.admin 2 | 3 | import com.cvillaseca.mobileapi.model.UserInfo 4 | import com.cvillaseca.mobileapi.service.UserService 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.ninjasquad.springmockk.MockkBean 7 | import io.mockk.every 8 | import io.mockk.verify 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.extension.ExtendWith 11 | import org.springframework.beans.factory.annotation.Autowired 12 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 13 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 14 | import org.springframework.http.MediaType 15 | import org.springframework.test.context.junit.jupiter.SpringExtension 16 | import org.springframework.test.web.servlet.MockMvc 17 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders 18 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers 19 | 20 | @ExtendWith(SpringExtension::class) 21 | @WebMvcTest(UserController::class) 22 | @AutoConfigureMockMvc(addFilters = false) 23 | internal class UserControllerTest { 24 | @MockkBean 25 | lateinit var mockUserService: UserService 26 | 27 | @Autowired 28 | private lateinit var mockMvc: MockMvc 29 | 30 | @Test 31 | fun `when the admin request the users, the user list is returned`() { 32 | val users = listOf( 33 | UserInfo(id = 3L, userId = 4L, email = "user@example.com", profileImage = "url"), 34 | UserInfo(id = 4L, userId = 5L, email = "user2@example.com", profileImage = "url2") 35 | ) 36 | val objectMapper = ObjectMapper() 37 | val userInfoJSON = objectMapper.writeValueAsString(users) 38 | every { mockUserService.getAllUsers() } returns users 39 | 40 | performUsers() 41 | .andExpect(MockMvcResultMatchers.status().isOk) 42 | .andExpect(MockMvcResultMatchers.content().json(userInfoJSON)) 43 | .andReturn() 44 | 45 | verify { mockUserService.getAllUsers() } 46 | } 47 | 48 | private fun performUsers() = 49 | mockMvc.perform(MockMvcRequestBuilders.get("/admin/users") 50 | .contentType(MediaType.APPLICATION_FORM_URLENCODED) 51 | ) 52 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/cvillaseca/mobileapi/controller/user/ChangePasswordControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.controller.user 2 | 3 | import com.cvillaseca.mobileapi.service.UserService 4 | import com.ninjasquad.springmockk.MockkBean 5 | import io.mockk.every 6 | import io.mockk.mockk 7 | import io.mockk.verify 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.extension.ExtendWith 10 | import org.springframework.beans.factory.annotation.Autowired 11 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 12 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 13 | import org.springframework.http.MediaType 14 | import org.springframework.test.context.junit.jupiter.SpringExtension 15 | import org.springframework.test.web.servlet.MockMvc 16 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post 17 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 18 | import java.security.Principal 19 | 20 | 21 | @ExtendWith(SpringExtension::class) 22 | @WebMvcTest(ChangePasswordController::class) 23 | @AutoConfigureMockMvc(addFilters = false) 24 | internal class ChangePasswordControllerTest { 25 | 26 | @MockkBean 27 | lateinit var mockUserService: UserService 28 | 29 | @Autowired 30 | private lateinit var mockMvc: MockMvc 31 | 32 | private val username = "user@example.com" 33 | 34 | private val mockPrincipal = mockk { 35 | every { name } returns username 36 | } 37 | 38 | @Test 39 | fun `when changing the password the old password matches, there is an ok response`() { 40 | val newPassword = "12345" 41 | val oldPassword = "48574" 42 | 43 | every { mockUserService.updatePassword(username, oldPassword, newPassword) } returns true 44 | 45 | performChangePassword(newPassword, oldPassword) 46 | .andExpect(status().isNoContent) 47 | .andReturn() 48 | 49 | verify { mockUserService.updatePassword(username, oldPassword, newPassword) } 50 | } 51 | 52 | @Test 53 | fun `when changing the password the old password does not match, there is an error`() { 54 | val newPassword = "123456" 55 | val oldPassword = "574874" 56 | 57 | every { mockUserService.updatePassword(username, oldPassword, newPassword) } returns false 58 | 59 | performChangePassword(newPassword, oldPassword) 60 | .andExpect(status().isBadRequest) 61 | .andReturn() 62 | 63 | every { mockUserService.updatePassword(username, oldPassword, newPassword) } 64 | } 65 | 66 | private fun performChangePassword(newPassword: String, oldPassword: String) = 67 | mockMvc.perform(post("/user/changePassword") 68 | .param("newPassword", newPassword) 69 | .param("oldPassword", oldPassword) 70 | .principal(mockPrincipal) 71 | .contentType(MediaType.APPLICATION_FORM_URLENCODED) 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/test/kotlin/com/cvillaseca/mobileapi/controller/user/MeControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.controller.user 2 | 3 | import com.cvillaseca.mobileapi.model.UserInfo 4 | import com.cvillaseca.mobileapi.service.UserService 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.ninjasquad.springmockk.MockkBean 7 | import io.mockk.every 8 | import io.mockk.mockk 9 | import io.mockk.verify 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | import org.springframework.beans.factory.annotation.Autowired 13 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 14 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 15 | import org.springframework.http.MediaType 16 | import org.springframework.test.context.junit.jupiter.SpringExtension 17 | import org.springframework.test.web.servlet.MockMvc 18 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders 19 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 20 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 21 | import java.security.Principal 22 | 23 | 24 | @ExtendWith(SpringExtension::class) 25 | @WebMvcTest(MeController::class) 26 | @AutoConfigureMockMvc(addFilters = false) 27 | internal class MeControllerTest { 28 | @MockkBean 29 | lateinit var mockUserService: UserService 30 | 31 | @Autowired 32 | private lateinit var mockMvc: MockMvc 33 | 34 | private val username = "user@example.com" 35 | 36 | private val mockPrincipal = mockk { 37 | every { name } returns username 38 | } 39 | 40 | @Test 41 | fun `when the user exists, the user info is returned`() { 42 | val expectedUserInfo = UserInfo(id = 3L, userId = 4L, email = "user@example.com", profileImage = "url") 43 | val objectMapper = ObjectMapper() 44 | val userInfoJSON = objectMapper.writeValueAsString(expectedUserInfo) 45 | every { mockUserService.getUserInfo(username) } returns expectedUserInfo 46 | 47 | performMe() 48 | .andExpect(status().isOk) 49 | .andExpect(content().json(userInfoJSON)) 50 | .andReturn() 51 | 52 | verify { mockUserService.getUserInfo(username) } 53 | } 54 | 55 | @Test 56 | fun `when the user does not exist, there is a not found error`() { 57 | every { mockUserService.getUserInfo(username) } returns null 58 | 59 | performMe() 60 | .andExpect(status().isNotFound) 61 | .andReturn() 62 | 63 | every { mockUserService.getUserInfo(username) } 64 | } 65 | 66 | private fun performMe() = 67 | mockMvc.perform(MockMvcRequestBuilders.get("/user/me") 68 | .principal(mockPrincipal) 69 | .contentType(MediaType.APPLICATION_FORM_URLENCODED) 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/test/kotlin/com/cvillaseca/mobileapi/controller/user/SignUpControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.controller.user 2 | 3 | import com.cvillaseca.mobileapi.model.User 4 | import com.cvillaseca.mobileapi.service.UserService 5 | import com.ninjasquad.springmockk.MockkBean 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import io.mockk.verify 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.extension.ExtendWith 11 | import org.springframework.beans.factory.annotation.Autowired 12 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 13 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 14 | import org.springframework.http.MediaType 15 | import org.springframework.test.context.junit.jupiter.SpringExtension 16 | import org.springframework.test.web.servlet.MockMvc 17 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders 18 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 19 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 20 | 21 | @ExtendWith(SpringExtension::class) 22 | @WebMvcTest(SignUpController::class) 23 | @AutoConfigureMockMvc(addFilters = false) 24 | internal class SignUpControllerTest { 25 | 26 | @MockkBean 27 | lateinit var mockUserService: UserService 28 | 29 | @Autowired 30 | private lateinit var mockMvc: MockMvc 31 | 32 | @Test 33 | fun `when a new user sign up, there is an ok response`() { 34 | val email = "newuser@example.com" 35 | val password = "1234" 36 | val newUserId = 50L 37 | val expectedUser = mockk { 38 | every { id } returns newUserId 39 | } 40 | 41 | every { mockUserService.createUser(email, password) } returns expectedUser 42 | 43 | performSignUp(email, password) 44 | .andExpect(status().isCreated) 45 | .andExpect(content().string(newUserId.toString())) 46 | .andReturn() 47 | 48 | verify { mockUserService.createUser(email, password) } 49 | } 50 | 51 | @Test 52 | fun `when a new user sign up with an existing email, there is an error response`() { 53 | val email = "existingemail@example.com" 54 | val password = "12345" 55 | 56 | every { mockUserService.createUser(email, password) } returns null 57 | 58 | performSignUp(email, password) 59 | .andExpect(status().isConflict) 60 | .andReturn() 61 | 62 | verify { mockUserService.createUser(email, password) } 63 | } 64 | 65 | private fun performSignUp(email: String, password: String) = 66 | mockMvc.perform(MockMvcRequestBuilders.post("/signUp") 67 | .param("email", email) 68 | .param("password", password) 69 | .contentType(MediaType.APPLICATION_FORM_URLENCODED) 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/test/kotlin/com/cvillaseca/mobileapi/service/UserServiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.cvillaseca.mobileapi.service 2 | 3 | import com.cvillaseca.mobileapi.dao.RoleDao 4 | import com.cvillaseca.mobileapi.dao.UserDao 5 | import com.cvillaseca.mobileapi.dao.UserInfoDao 6 | import com.cvillaseca.mobileapi.model.Role 7 | import com.cvillaseca.mobileapi.model.User 8 | import com.cvillaseca.mobileapi.model.UserInfo 9 | import com.ninjasquad.springmockk.MockkBean 10 | import io.mockk.every 11 | import io.mockk.mockk 12 | import io.mockk.verify 13 | import org.junit.jupiter.api.Assertions.assertEquals 14 | import org.junit.jupiter.api.Assertions.assertNull 15 | import org.junit.jupiter.api.Assertions.assertTrue 16 | import org.junit.jupiter.api.Test 17 | import org.junit.jupiter.api.assertThrows 18 | import org.junit.jupiter.api.extension.ExtendWith 19 | import org.springframework.beans.factory.annotation.Autowired 20 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 21 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 22 | import org.springframework.security.core.userdetails.UsernameNotFoundException 23 | import org.springframework.security.crypto.password.PasswordEncoder 24 | import org.springframework.test.context.junit.jupiter.SpringExtension 25 | 26 | @ExtendWith(SpringExtension::class) 27 | @WebMvcTest(UserService::class) 28 | @AutoConfigureMockMvc(addFilters = false) 29 | internal class UserServiceTest { 30 | @MockkBean 31 | lateinit var mockUserDao: UserDao 32 | 33 | @MockkBean 34 | lateinit var mockRoleDao: RoleDao 35 | 36 | @MockkBean 37 | lateinit var mockUserInfoDao: UserInfoDao 38 | 39 | @Autowired 40 | lateinit var passwordEncoder: PasswordEncoder 41 | 42 | @Autowired 43 | private lateinit var service: UserService 44 | 45 | private val username = "user@example.com" 46 | 47 | @Test 48 | fun `when the user is loaded by username, the user is returned`() { 49 | val expectedUser = mockk(relaxed = true) 50 | every { mockUserDao.findOneByUsername(username) } returns expectedUser 51 | 52 | val userDetails = service.loadUserByUsername(username) 53 | 54 | verify { mockUserDao.findOneByUsername(username) } 55 | assertEquals(expectedUser.username, userDetails.username) 56 | } 57 | 58 | @Test 59 | fun `when the user is not found, there is a UsernameNotFoundException`() { 60 | every { mockUserDao.findOneByUsername(username) } returns null 61 | 62 | assertThrows { 63 | service.loadUserByUsername(username) 64 | } 65 | 66 | verify { mockUserDao.findOneByUsername(username) } 67 | } 68 | 69 | @Test 70 | fun `when a new user is created successfully, the user info is returned`() { 71 | val userId = 3L 72 | val expectedUser = mockk(relaxed = true) { 73 | every { id } returns userId 74 | } 75 | val userRole = mockk(relaxed = true) 76 | val expectedNewUserInfo = UserInfo(userId = userId, email = username) 77 | every { mockUserDao.findOneByUsername(username) } returns null 78 | every { mockRoleDao.findByName("USER") } returns userRole 79 | every { mockUserDao.save(any()) } returns expectedUser 80 | every { mockUserInfoDao.save(expectedNewUserInfo) } returns expectedNewUserInfo 81 | 82 | val newUser = service.createUser(username, "password") 83 | 84 | verify { mockUserDao.findOneByUsername(username) } 85 | verify { mockRoleDao.findByName("USER") } 86 | verify { mockUserDao.save(any()) } 87 | verify { mockUserInfoDao.save(expectedNewUserInfo) } 88 | assertEquals(expectedUser, newUser) 89 | } 90 | 91 | @Test 92 | fun `when the new user already exists, a null is returned`() { 93 | val sameEmailUser = mockk(relaxed = true) 94 | every { mockUserDao.findOneByUsername(username) } returns sameEmailUser 95 | 96 | val newUser = service.createUser(username, "password") 97 | 98 | verify { mockUserDao.findOneByUsername(username) } 99 | assertNull(newUser) 100 | } 101 | 102 | @Test 103 | fun `when updating the password successfully, a true value is returned`() { 104 | val oldPassword = "1234" 105 | val newPassword = "newPassword" 106 | val expectedUser = mockk(relaxed = true) { 107 | every { password } returns passwordEncoder.encode(oldPassword) 108 | } 109 | every { mockUserDao.findOneByUsername(username) } returns expectedUser 110 | every { mockUserDao.save(any()) } returns expectedUser 111 | 112 | val passwordUpdated = service.updatePassword(username, oldPassword, newPassword) 113 | 114 | verify { mockUserDao.findOneByUsername(username) } 115 | verify { mockUserDao.save(any()) } 116 | assertTrue(passwordUpdated) 117 | } 118 | 119 | @Test 120 | fun `when updating the password the current password doesn't match, a false value is returned`() { 121 | val oldPassword = "wrongOldPassword" 122 | val newPassword = "newPassword" 123 | val expectedUser = mockk(relaxed = true) { 124 | every { password } returns passwordEncoder.encode("oldPassword") 125 | } 126 | every { mockUserDao.findOneByUsername(username) } returns expectedUser 127 | 128 | val passwordUpdated = service.updatePassword(username, oldPassword, newPassword) 129 | 130 | verify { mockUserDao.findOneByUsername(username) } 131 | assertTrue(passwordUpdated.not()) 132 | } 133 | 134 | @Test 135 | fun `when getting the user info successfully, the info is returned`() { 136 | val userId = 3L 137 | val expectedUser = mockk(relaxed = true) { 138 | every { id } returns userId 139 | } 140 | val expectedUserInfo = mockk(relaxed = true) 141 | every { mockUserDao.findOneByUsername(username) } returns expectedUser 142 | every { mockUserInfoDao.findOneByUserId(userId) } returns expectedUserInfo 143 | 144 | val userInfo = service.getUserInfo(username) 145 | 146 | verify { mockUserDao.findOneByUsername(username) } 147 | verify { mockUserInfoDao.findOneByUserId(userId) } 148 | assertEquals(expectedUserInfo, userInfo) 149 | } 150 | 151 | @Test 152 | fun `when getting the user info the info is not found, a null is returned`() { 153 | every { mockUserDao.findOneByUsername(username) } returns null 154 | 155 | val userInfo = service.getUserInfo(username) 156 | 157 | verify { mockUserDao.findOneByUsername(username) } 158 | assertNull(userInfo) 159 | } 160 | 161 | @Test 162 | fun `when getting the user list successfully, the list is returned`() { 163 | val expectedUsers = listOf(mockk(), mockk()) 164 | every { mockUserInfoDao.findAll() } returns expectedUsers 165 | 166 | val users = service.getAllUsers() 167 | 168 | verify { mockUserInfoDao.findAll() } 169 | assertEquals(expectedUsers, users) 170 | } 171 | } 172 | --------------------------------------------------------------------------------