diff --git a/Dockerfile b/Dockerfile index c3f593b..b925e75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:8-jdk as builder +FROM openjdk:11-jdk as builder MAINTAINER Billy Brawner RUN groupadd --system --gid 1000 maven \ @@ -11,8 +11,8 @@ COPY --chown=maven:maven . /home/maven/src WORKDIR /home/maven/src RUN /home/maven/src/mvnw -DskipTests package -FROM openjdk:8-jdk-slim +FROM openjdk:11-jdk-slim EXPOSE 8080 COPY --from=builder /home/maven/src/target/budget-api.jar budget-api.jar -ENTRYPOINT ["/usr/local/openjdk-8/bin/java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAMFraction=1", "-Xmx256m", "-jar", "/budget-api.jar"] +ENTRYPOINT ["/usr/local/openjdk-11/bin/java", "-Xmx256M", "-jar", "/budget-api.jar"] diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..3fe06de --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,68 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.net.URI + +buildscript { + repositories { + mavenLocal() + mavenCentral() +// maven { +// url = URI("https://repo.maven.apache.org/maven2") +// } + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61") + } +} + +plugins { + java + kotlin("jvm") version "1.3.61" + id("org.springframework.boot") version "2.2.4.RELEASE" +} + +apply(plugin = "io.spring.dependency-management") + +repositories { + mavenLocal() + mavenCentral() + maven { + url = URI("http://repo.maven.apache.org/maven2") + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.session:spring-session-jdbc") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.3.61") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61") + implementation("io.springfox:springfox-swagger2:2.8.0") + implementation("io.springfox:springfox-swagger-ui:2.8.0") + runtimeOnly("mysql:mysql-connector-java:8.0.15") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test:5.1.5.RELEASE") +} + +group = "com.wbrawner" +version = "0.0.1-SNAPSHOT" +description = "twigs-server" + +sourceSets.getByName("main") { + java.srcDir("src/main/kotlin") +} + +tasks.withType { + kotlinOptions.jvmTarget = "11" +} + +val mainClass = "com.wbrawner.budgetserver.BudgetServerApplication" +tasks.bootJar { + mainClassName = mainClass +} + +tasks.bootRun { + main = mainClass +} diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..2fe81a7 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..40507ab --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = 'budget-server' diff --git a/src/main/kotlin/com/wbrawner/budgetserver/BudgetServerApplication.kt b/src/main/kotlin/com/wbrawner/budgetserver/BudgetServerApplication.kt index 3477552..18875b1 100644 --- a/src/main/kotlin/com/wbrawner/budgetserver/BudgetServerApplication.kt +++ b/src/main/kotlin/com/wbrawner/budgetserver/BudgetServerApplication.kt @@ -4,8 +4,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication -class BudgetServerApplication - -fun main(args: Array) { - runApplication(*args) +open class BudgetServerApplication { + companion object { + @JvmStatic + fun main(args: Array) { + runApplication(*args) + } + } } diff --git a/src/main/kotlin/com/wbrawner/budgetserver/budget/BudgetController.kt b/src/main/kotlin/com/wbrawner/budgetserver/budget/BudgetController.kt index c4bd20d..7dedaf2 100644 --- a/src/main/kotlin/com/wbrawner/budgetserver/budget/BudgetController.kt +++ b/src/main/kotlin/com/wbrawner/budgetserver/budget/BudgetController.kt @@ -7,7 +7,6 @@ import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation import io.swagger.annotations.Authorization import org.hibernate.Hibernate -import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Sort import org.springframework.http.MediaType @@ -18,15 +17,15 @@ import javax.transaction.Transactional @RestController @RequestMapping("/budgets") @Api(value = "Budgets", tags = ["Budgets"], authorizations = [Authorization("basic")]) -class BudgetController @Autowired constructor( +@Transactional +open class BudgetController( private val budgetRepository: BudgetRepository, private val transactionRepository: TransactionRepository, private val userRepository: UserRepository ) { - @Transactional @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getBudgets", nickname = "getBudgets", tags = ["Budgets"]) - fun getBudgets(page: Int?, count: Int?): ResponseEntity> = ResponseEntity.ok( + open fun getBudgets(page: Int?, count: Int?): ResponseEntity> = ResponseEntity.ok( budgetRepository.findAllByUsersContainsOrOwner( user = getCurrentUser()!!, pageable = PageRequest.of(page ?: 0, count ?: 1000, Sort.by("name"))) @@ -36,20 +35,18 @@ class BudgetController @Autowired constructor( } ) - @Transactional @GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getBudget", nickname = "getBudget", tags = ["Budgets"]) - fun getBudget(@PathVariable id: Long): ResponseEntity = budgetRepository.findByUsersContainsAndId(getCurrentUser()!!, id) + open fun getBudget(@PathVariable id: Long): ResponseEntity = budgetRepository.findByUsersContainsAndId(getCurrentUser()!!, id) .orElse(null) ?.let { Hibernate.initialize(it.users) ResponseEntity.ok(BudgetResponse(it)) } ?: ResponseEntity.notFound().build() - @Transactional @GetMapping("/{id}/balance", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getBudgetBalance", nickname = "getBudgetBalance", tags = ["Budgets"]) - fun getBudgetBalance(@PathVariable id: Long): ResponseEntity = + open fun getBudgetBalance(@PathVariable id: Long): ResponseEntity = budgetRepository.findByUsersContainsAndId(getCurrentUser()!!, id) .orElse(null) ?.let { @@ -58,7 +55,7 @@ class BudgetController @Autowired constructor( @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "newBudget", nickname = "newBudget", tags = ["Budgets"]) - fun newBudget(@RequestBody request: NewBudgetRequest): ResponseEntity { + open fun newBudget(@RequestBody request: NewBudgetRequest): ResponseEntity { val users = request.userIds .map { id -> userRepository.findById(id).orElse(null) } .filterNotNull() @@ -70,7 +67,7 @@ class BudgetController @Autowired constructor( @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "updateBudget", nickname = "updateBudget", tags = ["Budgets"]) - fun updateBudget(@PathVariable id: Long, request: UpdateBudgetRequest): ResponseEntity { + open fun updateBudget(@PathVariable id: Long, request: UpdateBudgetRequest): ResponseEntity { var budget = budgetRepository.findByUsersContainsAndId(getCurrentUser()!!, id).orElse(null) ?: return ResponseEntity.notFound().build() if (request.name != null) budget = budget.copy(name = request.name) @@ -81,7 +78,7 @@ class BudgetController @Autowired constructor( @DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE]) @ApiOperation(value = "deleteBudget", nickname = "deleteBudget", tags = ["Budgets"]) - fun deleteBudget(@PathVariable id: Long): ResponseEntity { + open fun deleteBudget(@PathVariable id: Long): ResponseEntity { val budget = budgetRepository.findByUsersContainsAndId(getCurrentUser()!!, id).orElse(null) ?: return ResponseEntity.notFound().build() budgetRepository.delete(budget) diff --git a/src/main/kotlin/com/wbrawner/budgetserver/category/CategoryController.kt b/src/main/kotlin/com/wbrawner/budgetserver/category/CategoryController.kt index 1236123..353c306 100644 --- a/src/main/kotlin/com/wbrawner/budgetserver/category/CategoryController.kt +++ b/src/main/kotlin/com/wbrawner/budgetserver/category/CategoryController.kt @@ -3,34 +3,31 @@ package com.wbrawner.budgetserver.category import com.wbrawner.budgetserver.ErrorResponse import com.wbrawner.budgetserver.budget.BudgetRepository import com.wbrawner.budgetserver.getCurrentUser -import com.wbrawner.budgetserver.setToFirstOfMonth import com.wbrawner.budgetserver.transaction.TransactionRepository import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation import io.swagger.annotations.Authorization import org.hibernate.Hibernate -import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Sort import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import java.lang.Integer.min -import java.util.* import javax.transaction.Transactional @RestController @RequestMapping("/categories") @Api(value = "Categories", tags = ["Categories"], authorizations = [Authorization("basic")]) -class CategoryController @Autowired constructor( +@Transactional +open class CategoryController( private val budgetRepository: BudgetRepository, private val categoryRepository: CategoryRepository, private val transactionRepository: TransactionRepository ) { - @Transactional @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getCategories", nickname = "getCategories", tags = ["Categories"]) - fun getCategories(budgetId: Long? = null, + open fun getCategories(budgetId: Long? = null, isExpense: Boolean? = null, count: Int?, page: Int?, @@ -43,9 +40,9 @@ class CategoryController @Autowired constructor( budgetRepository.findAllByUsersContainsOrOwner(getCurrentUser()!!) }.toList() val pageRequest = PageRequest.of( - min(0, page?.minus(1)?: 0), - count?: 1000, - Sort(sortOrder?: Sort.Direction.ASC, sortBy?: "title") + min(0, page?.minus(1) ?: 0), + count ?: 1000, + Sort.by(sortOrder ?: Sort.Direction.ASC, sortBy ?: "title") ) val categories = if (isExpense == null) { categoryRepository.findAllByBudgetIn(budgets, pageRequest) @@ -57,16 +54,16 @@ class CategoryController @Autowired constructor( @GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getCategory", nickname = "getCategory", tags = ["Categories"]) - fun getCategory(@PathVariable id: Long): ResponseEntity { + open fun getCategory(@PathVariable id: Long): ResponseEntity { val category = categoryRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() budgetRepository.findByUsersContainsAndCategoriesContains(getCurrentUser()!!, category).orElse(null) ?: return ResponseEntity.notFound().build() - return ResponseEntity.ok(CategoryResponse(category)) + return ResponseEntity.ok(CategoryResponse(category)) } @GetMapping("/{id}/balance", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getCategoryBalance", nickname = "getCategoryBalance", tags = ["Categories"]) - fun getCategoryBalance(@PathVariable id: Long): ResponseEntity { + open fun getCategoryBalance(@PathVariable id: Long): ResponseEntity { val category = categoryRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() budgetRepository.findByUsersContainsAndCategoriesContains(getCurrentUser()!!, category).orElse(null) ?: return ResponseEntity.notFound().build() @@ -74,10 +71,9 @@ class CategoryController @Autowired constructor( return ResponseEntity.ok(CategoryBalanceResponse(category.id, transactions)) } - @Transactional @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "newCategory", nickname = "newCategory", tags = ["Categories"]) - fun newCategory(@RequestBody request: NewCategoryRequest): ResponseEntity { + open fun newCategory(@RequestBody request: NewCategoryRequest): ResponseEntity { val budget = budgetRepository.findByUsersContainsAndId(getCurrentUser()!!, request.budgetId).orElse(null) ?: return ResponseEntity.badRequest().body(ErrorResponse("Invalid budget ID")) Hibernate.initialize(budget.users) @@ -91,7 +87,7 @@ class CategoryController @Autowired constructor( @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "updateCategory", nickname = "updateCategory", tags = ["Categories"]) - fun updateCategory(@PathVariable id: Long, @RequestBody request: UpdateCategoryRequest): ResponseEntity { + open fun updateCategory(@PathVariable id: Long, @RequestBody request: UpdateCategoryRequest): ResponseEntity { var category = categoryRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() budgetRepository.findByUsersContainsAndCategoriesContains(getCurrentUser()!!, category).orElse(null) ?: return ResponseEntity.notFound().build() @@ -103,7 +99,7 @@ class CategoryController @Autowired constructor( @DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE]) @ApiOperation(value = "deleteCategory", nickname = "deleteCategory", tags = ["Categories"]) - fun deleteCategory(@PathVariable id: Long): ResponseEntity { + open fun deleteCategory(@PathVariable id: Long): ResponseEntity { val category = categoryRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() val budget = budgetRepository.findByUsersContainsAndCategoriesContains(getCurrentUser()!!, category).orElse(null) ?: return ResponseEntity.notFound().build() diff --git a/src/main/kotlin/com/wbrawner/budgetserver/config/JdbcUserDetailsService.kt b/src/main/kotlin/com/wbrawner/budgetserver/config/JdbcUserDetailsService.kt index b268070..d522eee 100644 --- a/src/main/kotlin/com/wbrawner/budgetserver/config/JdbcUserDetailsService.kt +++ b/src/main/kotlin/com/wbrawner/budgetserver/config/JdbcUserDetailsService.kt @@ -8,7 +8,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Component @Component -class JdbcUserDetailsService @Autowired +open class JdbcUserDetailsService @Autowired constructor(private val userRepository: UserRepository) : UserDetailsService { @Throws(UsernameNotFoundException::class) diff --git a/src/main/kotlin/com/wbrawner/budgetserver/config/SecurityConfig.kt b/src/main/kotlin/com/wbrawner/budgetserver/config/SecurityConfig.kt index 7bc97c3..e5272b2 100644 --- a/src/main/kotlin/com/wbrawner/budgetserver/config/SecurityConfig.kt +++ b/src/main/kotlin/com/wbrawner/budgetserver/config/SecurityConfig.kt @@ -20,15 +20,15 @@ import javax.sql.DataSource @Configuration @EnableWebSecurity -class SecurityConfig @Autowired -constructor( +open class SecurityConfig( private val env: Environment, private val datasource: DataSource, private val userRepository: UserRepository, private val passwordResetRequestRepository: PasswordResetRequestRepository, - private val userDetailsService: JdbcUserDetailsService) : WebSecurityConfigurerAdapter() { + private val userDetailsService: JdbcUserDetailsService +) : WebSecurityConfigurerAdapter() { - val userDetailsManager: JdbcUserDetailsManager + open val userDetailsManager: JdbcUserDetailsManager @Bean get() { val userDetailsManager = JdbcUserDetailsManager() @@ -36,14 +36,14 @@ constructor( return userDetailsManager } - val authenticationProvider: DaoAuthenticationProvider + open val authenticationProvider: DaoAuthenticationProvider @Bean get() = DaoAuthenticationProvider().apply { this.setPasswordEncoder(passwordEncoder) this.setUserDetailsService(userDetailsService) } - val passwordEncoder: PasswordEncoder + open val passwordEncoder: PasswordEncoder @Bean get() = BCryptPasswordEncoder() @@ -70,5 +70,5 @@ constructor( @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) -class MethodSecurity : GlobalMethodSecurityConfiguration() +open class MethodSecurity : GlobalMethodSecurityConfiguration() diff --git a/src/main/kotlin/com/wbrawner/budgetserver/config/SwaggerConfig.kt b/src/main/kotlin/com/wbrawner/budgetserver/config/SwaggerConfig.kt index 459efe0..33eea5e 100644 --- a/src/main/kotlin/com/wbrawner/budgetserver/config/SwaggerConfig.kt +++ b/src/main/kotlin/com/wbrawner/budgetserver/config/SwaggerConfig.kt @@ -12,9 +12,9 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2 @Configuration @EnableSwagger2 -class SwaggerConfig : WebMvcConfigurationSupport() { +open class SwaggerConfig : WebMvcConfigurationSupport() { @Bean - fun budgetApi(): Docket = Docket(DocumentationType.SWAGGER_2) + open fun budgetApi(): Docket = Docket(DocumentationType.SWAGGER_2) .securitySchemes(mutableListOf(BasicAuth("basic"))) .select() .apis(RequestHandlerSelectors.basePackage("com.wbrawner.budgetserver")) diff --git a/src/main/kotlin/com/wbrawner/budgetserver/transaction/TransactionController.kt b/src/main/kotlin/com/wbrawner/budgetserver/transaction/TransactionController.kt index e5ac21f..9486bd9 100644 --- a/src/main/kotlin/com/wbrawner/budgetserver/transaction/TransactionController.kt +++ b/src/main/kotlin/com/wbrawner/budgetserver/transaction/TransactionController.kt @@ -26,17 +26,17 @@ import javax.transaction.Transactional @RestController @RequestMapping("/transactions") @Api(value = "Transactions", tags = ["Transactions"], authorizations = [Authorization("basic")]) -class TransactionController @Autowired constructor( +@Transactional +open class TransactionController( private val budgetRepository: BudgetRepository, private val categoryRepository: CategoryRepository, private val transactionRepository: TransactionRepository ) { private val logger = LoggerFactory.getLogger(TransactionController::class.java) - @Transactional @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getTransactions", nickname = "getTransactions", tags = ["Transactions"]) - fun getTransactions( + open fun getTransactions( @RequestParam("categoryId") categoryIds: Array? = null, @RequestParam("budgetId") budgetIds: Array? = null, @RequestParam("from") from: String? = null, @@ -90,17 +90,16 @@ class TransactionController @Autowired constructor( @GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getTransaction", nickname = "getTransaction", tags = ["Transactions"]) - fun getTransaction(@PathVariable id: Long): ResponseEntity { + open fun getTransaction(@PathVariable id: Long): ResponseEntity { val transaction = transactionRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() budgetRepository.findByUsersContainsAndTransactionsContains(getCurrentUser()!!, transaction).orElse(null) ?: return ResponseEntity.notFound().build() return ResponseEntity.ok(TransactionResponse(transaction)) } - @Transactional @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "newTransaction", nickname = "newTransaction", tags = ["Transactions"]) - fun newTransaction(@RequestBody request: NewTransactionRequest): ResponseEntity { + open fun newTransaction(@RequestBody request: NewTransactionRequest): ResponseEntity { val budget = budgetRepository.findByUsersContainsAndId(getCurrentUser()!!, request.budgetId).orElse(null) ?: return ResponseEntity.badRequest().body(ErrorResponse("Invalid budget ID")) Hibernate.initialize(budget.users) @@ -121,7 +120,7 @@ class TransactionController @Autowired constructor( @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "updateTransaction", nickname = "updateTransaction", tags = ["Transactions"]) - fun updateTransaction(@PathVariable id: Long, @RequestBody request: UpdateTransactionRequest): ResponseEntity { + open fun updateTransaction(@PathVariable id: Long, @RequestBody request: UpdateTransactionRequest): ResponseEntity { var transaction = transactionRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() var budget = budgetRepository.findByUsersContainsAndTransactionsContains(getCurrentUser()!!, transaction) .orElse(null) ?: return ResponseEntity.notFound().build() @@ -146,7 +145,7 @@ class TransactionController @Autowired constructor( @DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE]) @ApiOperation(value = "deleteTransaction", nickname = "deleteTransaction", tags = ["Transactions"]) - fun deleteTransaction(@PathVariable id: Long): ResponseEntity { + open fun deleteTransaction(@PathVariable id: Long): ResponseEntity { val transaction = transactionRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build() // Check that the transaction belongs to an budget that the user has access to before deleting it budgetRepository.findByUsersContainsAndTransactionsContains(getCurrentUser()!!, transaction).orElse(null) diff --git a/src/main/kotlin/com/wbrawner/budgetserver/user/UserController.kt b/src/main/kotlin/com/wbrawner/budgetserver/user/UserController.kt index 34f0878..e039e04 100644 --- a/src/main/kotlin/com/wbrawner/budgetserver/user/UserController.kt +++ b/src/main/kotlin/com/wbrawner/budgetserver/user/UserController.kt @@ -21,17 +21,17 @@ import javax.transaction.Transactional @RestController @RequestMapping("/users") @Api(value = "Users", tags = ["Users"], authorizations = [Authorization("basic")]) -class UserController @Autowired constructor( +@Transactional +open class UserController( private val budgetRepository: BudgetRepository, private val userRepository: UserRepository, private val passwordEncoder: PasswordEncoder, private val authenticationProvider: DaoAuthenticationProvider ) { - @Transactional @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getUsers", nickname = "getUsers", tags = ["Users"]) - fun getUsers(budgetId: Long): ResponseEntity> { + open fun getUsers(budgetId: Long): ResponseEntity> { val budget = budgetRepository.findByUsersContainsAndId(getCurrentUser()!!, budgetId).orElse(null) ?: return ResponseEntity.notFound().build() Hibernate.initialize(budget.users) @@ -40,7 +40,7 @@ class UserController @Autowired constructor( @PostMapping("/login", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "login", nickname = "login", tags = ["Users"]) - fun login(@RequestBody request: LoginRequest): ResponseEntity { + open fun login(@RequestBody request: LoginRequest): ResponseEntity { val authReq = UsernamePasswordAuthenticationToken(request.username, request.password) val auth = try { authenticationProvider.authenticate(authReq) @@ -53,21 +53,20 @@ class UserController @Autowired constructor( @GetMapping("/me", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "getProfile", nickname = "getProfile", tags = ["Users"]) - fun getProfile(): ResponseEntity { + open fun getProfile(): ResponseEntity { val user = getCurrentUser()?: return ResponseEntity.status(401).build() return ResponseEntity.ok(UserResponse(user)) } - @Transactional @GetMapping("/search", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "searchUsers", nickname = "searchUsers", tags = ["Users"]) - fun searchUsers(query: String): ResponseEntity> { + open fun searchUsers(query: String): ResponseEntity> { return ResponseEntity.ok(userRepository.findByNameContains(query).map { UserResponse(it) }) } @GetMapping("/{id}") @ApiOperation(value = "getUser", nickname = "getUser", tags = ["Users"]) - fun getUser(@PathVariable id: Long): ResponseEntity = userRepository.findById(id).orElse(null) + open fun getUser(@PathVariable id: Long): ResponseEntity = userRepository.findById(id).orElse(null) ?.let { ResponseEntity.ok(UserResponse(it)) } @@ -75,7 +74,7 @@ class UserController @Autowired constructor( @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "newUser", nickname = "newUser", tags = ["Users"]) - fun newUser(@RequestBody request: NewUserRequest): ResponseEntity { + open fun newUser(@RequestBody request: NewUserRequest): ResponseEntity { if (userRepository.findByName(request.username).isPresent) return ResponseEntity.badRequest() .body(ErrorResponse("Username taken")) @@ -94,7 +93,7 @@ class UserController @Autowired constructor( @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "updateUser", nickname = "updateUser", tags = ["Users"]) - fun updateUser(@PathVariable id: Long, @RequestBody request: UpdateUserRequest): ResponseEntity { + open fun updateUser(@PathVariable id: Long, @RequestBody request: UpdateUserRequest): ResponseEntity { if (getCurrentUser()!!.id != id) return ResponseEntity.status(403) .body(ErrorResponse("Attempting to modify another user's budget")) var user = userRepository.findById(getCurrentUser()!!.id!!).orElse(null)?: return ResponseEntity.notFound().build() @@ -115,7 +114,7 @@ class UserController @Autowired constructor( @DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE]) @ApiOperation(value = "deleteUser", nickname = "deleteUser", tags = ["Users"]) - fun deleteUser(@PathVariable id: Long): ResponseEntity { + open fun deleteUser(@PathVariable id: Long): ResponseEntity { if(getCurrentUser()!!.id != id) return ResponseEntity.status(403).build() userRepository.deleteById(id) return ResponseEntity.ok().build() diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ab5bfef..8d664d0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,7 +4,6 @@ spring.datasource.username=budget spring.datasource.password=budget spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.profiles.active=prod -spring.session.store-type=jdbc spring.session.jdbc.initialize-schema=always spring.datasource.testWhileIdle=true spring.datasource.timeBetweenEvictionRunsMillis=60000