├── .gitignore ├── .gradle ├── 6.6.1 │ ├── executionHistory │ │ ├── executionHistory.bin │ │ └── executionHistory.lock │ ├── fileChanges │ │ └── last-build.bin │ ├── fileContent │ │ └── fileContent.lock │ ├── fileHashes │ │ ├── fileHashes.bin │ │ ├── fileHashes.lock │ │ └── resourceHashesCache.bin │ └── gc.properties ├── buildOutputCleanup │ ├── buildOutputCleanup.lock │ ├── cache.properties │ └── outputFiles.bin ├── checksums │ ├── checksums.lock │ ├── md5-checksums.bin │ └── sha1-checksums.bin ├── configuration-cache │ └── gc.properties └── vcs-1 │ └── gc.properties ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── libraries-with-intellij-classes.xml ├── misc.xml ├── runConfigurations.xml ├── uiDesigner.xml └── vcs.xml ├── Dockerfile ├── README.md ├── build.gradle ├── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── postman └── kotlin-ktor-rest-api.postman_collection.json ├── resources ├── application.conf └── logback.xml ├── settings.gradle ├── src ├── Application.kt ├── RouteManager.kt ├── di │ └── ApplicationModule.kt ├── errors │ └── GenericServerError.kt ├── extensions │ ├── ApplicationCallExt.kt │ └── DatabaseExt.kt ├── features │ ├── authentication │ │ ├── dao │ │ │ ├── AuthenticationDao.kt │ │ │ ├── AuthenticationDaoImpl.kt │ │ │ ├── entity │ │ │ │ └── User.kt │ │ │ └── mapper │ │ │ │ ├── AuthenticationMapper.kt │ │ │ │ └── AuthenticationMapperImpl.kt │ │ ├── data │ │ │ ├── AuthenticationData.kt │ │ │ └── AuthenticationDataImpl.kt │ │ ├── di │ │ │ └── AuthenticationModule.kt │ │ ├── model │ │ │ ├── LoginRequestDto.kt │ │ │ └── UserInfoDto.kt │ │ └── routes │ │ │ ├── AuthenticationRoutes.kt │ │ │ ├── createUser │ │ │ └── CreateUser.kt │ │ │ ├── loginUser │ │ │ └── LoginUser.kt │ │ │ └── userInfo │ │ │ └── UserInfo.kt │ ├── healthcheck │ │ ├── data │ │ │ ├── HealthCheckData.kt │ │ │ └── HealthCheckDataImpl.kt │ │ ├── di │ │ │ └── HealthCheckModule.kt │ │ └── routes │ │ │ ├── HealthCheckRoutes.kt │ │ │ └── gethealthcheck │ │ │ └── GetHealthCheck.kt │ └── starwars │ │ ├── data │ │ ├── StarWarsData.kt │ │ └── StarWarsDataImpl.kt │ │ ├── di │ │ └── StarWarsModule.kt │ │ ├── model │ │ └── Movie.kt │ │ ├── remote │ │ ├── StarWarsRemote.kt │ │ └── StarWarsRemoteImpl.kt │ │ └── routes │ │ ├── StarWarsRoutes.kt │ │ └── getmovie │ │ └── GetMovie.kt ├── httpclient │ ├── HttpService.kt │ └── HttpServiceImpl.kt └── jwt │ ├── JwtManager.kt │ └── JwtManagerImpl.kt └── test └── ApplicationTest.kt /.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 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .gradle/ 26 | build/ 27 | -------------------------------------------------------------------------------- /.gradle/6.6.1/executionHistory/executionHistory.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/6.6.1/executionHistory/executionHistory.bin -------------------------------------------------------------------------------- /.gradle/6.6.1/executionHistory/executionHistory.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/6.6.1/executionHistory/executionHistory.lock -------------------------------------------------------------------------------- /.gradle/6.6.1/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gradle/6.6.1/fileContent/fileContent.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/6.6.1/fileContent/fileContent.lock -------------------------------------------------------------------------------- /.gradle/6.6.1/fileHashes/fileHashes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/6.6.1/fileHashes/fileHashes.bin -------------------------------------------------------------------------------- /.gradle/6.6.1/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/6.6.1/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /.gradle/6.6.1/fileHashes/resourceHashesCache.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/6.6.1/fileHashes/resourceHashesCache.bin -------------------------------------------------------------------------------- /.gradle/6.6.1/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/6.6.1/gc.properties -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/buildOutputCleanup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/buildOutputCleanup/buildOutputCleanup.lock -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/cache.properties: -------------------------------------------------------------------------------- 1 | #Sun Oct 02 10:46:58 TRT 2022 2 | gradle.version=7.0 3 | -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/outputFiles.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/buildOutputCleanup/outputFiles.bin -------------------------------------------------------------------------------- /.gradle/checksums/checksums.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/checksums/checksums.lock -------------------------------------------------------------------------------- /.gradle/checksums/md5-checksums.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/checksums/md5-checksums.bin -------------------------------------------------------------------------------- /.gradle/checksums/sha1-checksums.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/checksums/sha1-checksums.bin -------------------------------------------------------------------------------- /.gradle/configuration-cache/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/configuration-cache/gc.properties -------------------------------------------------------------------------------- /.gradle/vcs-1/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selimatasoy/kotlin-ktor-rest-api/b6e19191252f422e7f2c93f96cafaacfd3ac2393/.gradle/vcs-1/gc.properties -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | kotlin-ktor-rest-api -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/libraries-with-intellij-classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 64 | 65 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gradle:7-jdk11 AS build 2 | COPY --chown=gradle:gradle . /home/gradle/src 3 | WORKDIR /home/gradle/src 4 | RUN gradle shadowJar --no-daemon 5 | 6 | FROM amazoncorretto:11 7 | EXPOSE 8081:8081 8 | RUN mkdir /app 9 | COPY --from=build /home/gradle/src/build/libs/*.jar /app/kotlin-ktor-rest-api.jar 10 | ENTRYPOINT ["java","-jar","/app/kotlin-ktor-rest-api.jar"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kotlin-ktor-rest-api 2 | A Modern Kotlin-Ktor RESTful API example. Connects to a PostgreSQL database and uses Exposed framework for database 3 | operations. Focused on Software Architecture, SOLID Principles, dependency injection, testable code and feature based 4 | development. 5 | 6 | - Ktor Framework 7 | - Kotlin 8 | - Dependency Injection -> Koin 9 | - Authorization -> JWT 10 | - Database -> PostgreSQL 11 | - ORM SQL Framework -> Exposed 12 | - Ktor Client -> For External API calls 13 | - Build Tool -> Gradle 14 | - Server -> Tomcat 15 | - Docker support for containerization 16 | - POSTMAN Collection for testing API 17 | - Gradle 7, JDK (Amazon Coretto 11) 18 | 19 | # Architecture 20 | ![github (6)](https://user-images.githubusercontent.com/86873858/131125468-99d372c5-2b55-473b-9f12-0fbd2c7e9bf7.png) 21 | 22 | # Database Credentials (Please add your PostgreSQL credentials here to connect to your database) 23 | resources/application.conf 24 | 25 | ``` 26 | database { 27 | exampleDatabaseUrl="jdbc:postgresql://localhost:5432/$YOUR_DB_NAME" 28 | exampleDatabaseDriver="org.postgresql.Driver" 29 | exampleDatabaseUser="$YOUR_USERNAME" 30 | exampleDatabasePassword="$YOUR_PASSWORD" 31 | } 32 | ``` 33 | # Features 34 | ## - Health Check 35 | 36 | **GET /public-api/v1/healthCheck**
37 |   Just returns a simple object
38 | ## - Authentication 39 | 40 | **POST /public-api/v1/authentication/createUser**
41 |   Registers a user to the db
42 | **POST /public-api/v1/authentication/login**
43 |   Returns a jwt token if success
44 | **GET /api/v1/authentication/userInfo** (Needs Authorization token from login as Authorization header : "Bearer 45 | $token")
46 |   Returns the user information
47 | ## - Star Wars 48 | 49 | **GET /api/v1/star-wars/movie** (Needs Authorization token from login as Authorization header : "Bearer $token")
50 |   An External API call example. Returns a movie information from an external api 51 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 8 | } 9 | } 10 | 11 | plugins { 12 | id "com.github.johnrengelman.shadow" version "7.1.2" 13 | } 14 | 15 | apply plugin: 'java' 16 | apply plugin: 'kotlin' 17 | apply plugin: 'application' 18 | 19 | application { 20 | mainClass = "io.ktor.server.tomcat.EngineMain" 21 | } 22 | 23 | group 'com.selimatasoy' 24 | version '0.0.1' 25 | 26 | sourceSets { 27 | main.kotlin.srcDirs = main.java.srcDirs = ['src'] 28 | test.kotlin.srcDirs = test.java.srcDirs = ['test'] 29 | main.resources.srcDirs = ['resources'] 30 | test.resources.srcDirs = ['testresources'] 31 | } 32 | 33 | repositories { 34 | mavenLocal() 35 | jcenter() 36 | maven { url 'https://kotlin.bintray.com/ktor' } 37 | } 38 | 39 | dependencies { 40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 41 | implementation "io.ktor:ktor-server-tomcat:$ktor_version" 42 | implementation "ch.qos.logback:logback-classic:$logback_version" 43 | implementation "io.ktor:ktor-server-core:$ktor_version" 44 | implementation "io.ktor:ktor-server-auth:$ktor_version" 45 | implementation "io.ktor:ktor-server-auth-jwt:$ktor_version" 46 | implementation "io.ktor:ktor-server-status-pages:$ktor_version" 47 | implementation "io.ktor:ktor-server-call-logging:$ktor_version" 48 | 49 | implementation "io.ktor:ktor-server-content-negotiation:$ktor_version" 50 | implementation "io.ktor:ktor-serialization-jackson:$ktor_version" 51 | implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" 52 | 53 | implementation "io.ktor:ktor-client-okhttp:$ktor_version" 54 | implementation "io.ktor:ktor-client-content-negotiation:$ktor_version" 55 | implementation "io.ktor:ktor-serialization-gson:$ktor_version" 56 | implementation "io.ktor:ktor-client-logging-jvm:${ktor_version}" 57 | implementation "ch.qos.logback:logback-classic:1.2.5" 58 | 59 | implementation ("io.insert-koin:koin-ktor:$koin_version") 60 | implementation ("io.insert-koin:koin-logger-slf4j:$koin_version") 61 | testImplementation "io.insert-koin:koin-test:$koin_version" 62 | testImplementation "io.insert-koin:koin-test-junit4:$koin_version" 63 | 64 | implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") 65 | implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") 66 | implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion") 67 | implementation("org.jetbrains.exposed:exposed-jodatime:$exposedVersion") 68 | implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") 69 | implementation("org.slf4j:slf4j-nop:1.7.30") 70 | implementation("org.postgresql:postgresql:42.2.2") 71 | 72 | testImplementation "io.ktor:ktor-server-tests:$ktor_version" 73 | } 74 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "8081:8081" -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | logback_version=1.2.1 2 | ktor_version=2.2.3 3 | kotlin.code.style=official 4 | kotlin_version=1.7.21 5 | koin_version=3.2.2 6 | exposedVersion=0.41.1 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /postman/kotlin-ktor-rest-api.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "7a63e842-e667-41b9-9a94-3f5925c12b2a", 4 | "name": "kotlin-ktor-rest-api", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "authentication", 10 | "item": [ 11 | { 12 | "name": "createUser", 13 | "request": { 14 | "method": "POST", 15 | "header": [], 16 | "body": { 17 | "mode": "raw", 18 | "raw": "{\"name\": \"selim\",\"surname\":\"atasoy\",\"birthDate\":\"01.01.1991\",\"email\":\"selim@gmail.com\", \"password\": \"TestTest\"}", 19 | "options": { 20 | "raw": { 21 | "language": "json" 22 | } 23 | } 24 | }, 25 | "url": { 26 | "raw": "http://localhost:8081/public-api/v1/authentication/createUser", 27 | "protocol": "http", 28 | "host": [ 29 | "localhost" 30 | ], 31 | "port": "8081", 32 | "path": [ 33 | "public-api", 34 | "v1", 35 | "authentication", 36 | "createUser" 37 | ] 38 | } 39 | }, 40 | "response": [] 41 | }, 42 | { 43 | "name": "login", 44 | "request": { 45 | "method": "POST", 46 | "header": [], 47 | "body": { 48 | "mode": "raw", 49 | "raw": "{\"email\":\"selim@gmail.com\", \"password\": \"TestTest\"}", 50 | "options": { 51 | "raw": { 52 | "language": "json" 53 | } 54 | } 55 | }, 56 | "url": { 57 | "raw": "http://localhost:8081/public-api/v1/authentication/login", 58 | "protocol": "http", 59 | "host": [ 60 | "localhost" 61 | ], 62 | "port": "8081", 63 | "path": [ 64 | "public-api", 65 | "v1", 66 | "authentication", 67 | "login" 68 | ] 69 | } 70 | }, 71 | "response": [] 72 | }, 73 | { 74 | "name": "userInfo", 75 | "request": { 76 | "method": "GET", 77 | "header": [ 78 | { 79 | "key": "Authorization", 80 | "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBdXRoZW50aWNhdGlvbiIsImV4cCI6MTY2NDcwMTAzNiwiZW1haWwiOiJzZWxpbUBnbWFpbC5jb20ifQ.uCP0qpGFyVgl8mYPrgzMSXJ6VhDT49cYCZ9K_LAPtGI", 81 | "type": "default" 82 | } 83 | ], 84 | "url": { 85 | "raw": "http://localhost:8081/api/v1/authentication/userInfo", 86 | "protocol": "http", 87 | "host": [ 88 | "localhost" 89 | ], 90 | "port": "8081", 91 | "path": [ 92 | "api", 93 | "v1", 94 | "authentication", 95 | "userInfo" 96 | ] 97 | } 98 | }, 99 | "response": [] 100 | } 101 | ] 102 | }, 103 | { 104 | "name": "healthcheck", 105 | "item": [ 106 | { 107 | "name": "healthCheck", 108 | "request": { 109 | "method": "GET", 110 | "header": [], 111 | "url": { 112 | "raw": "http://localhost:8081/public-api/v1/healthCheck", 113 | "protocol": "http", 114 | "host": [ 115 | "localhost" 116 | ], 117 | "port": "8081", 118 | "path": [ 119 | "public-api", 120 | "v1", 121 | "healthCheck" 122 | ] 123 | } 124 | }, 125 | "response": [] 126 | } 127 | ] 128 | }, 129 | { 130 | "name": "getmovie", 131 | "item": [ 132 | { 133 | "name": "http://localhost:8080/api/v1/star-wars/movie", 134 | "request": { 135 | "method": "GET", 136 | "header": [ 137 | { 138 | "key": "Authorization", 139 | "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBdXRoZW50aWNhdGlvbiIsImV4cCI6MTY2NDcwMTAzNiwiZW1haWwiOiJzZWxpbUBnbWFpbC5jb20ifQ.uCP0qpGFyVgl8mYPrgzMSXJ6VhDT49cYCZ9K_LAPtGI", 140 | "type": "default" 141 | } 142 | ], 143 | "url": { 144 | "raw": "http://localhost:8081/api/v1/star-wars/movie", 145 | "protocol": "http", 146 | "host": [ 147 | "localhost" 148 | ], 149 | "port": "8081", 150 | "path": [ 151 | "api", 152 | "v1", 153 | "star-wars", 154 | "movie" 155 | ] 156 | } 157 | }, 158 | "response": [] 159 | } 160 | ] 161 | } 162 | ] 163 | } -------------------------------------------------------------------------------- /resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8081 4 | } 5 | application { 6 | modules = [ com.selimatasoy.ApplicationKt.module ] 7 | } 8 | } 9 | 10 | database { 11 | exampleDatabaseUrl="jdbc:postgresql://localhost:5432/exampleDatabase" 12 | exampleDatabaseDriver="org.postgresql.Driver" 13 | exampleDatabaseUser="postgres" 14 | exampleDatabasePassword="Test1234" 15 | } 16 | 17 | jwt { 18 | "secret" = "123" 19 | "validity_ms" = "36000000" // 10 Hours 20 | "issuer" = "selimatasoy" 21 | "realm" = "selimatasoy.kotlin-ktor-rest-api" 22 | } 23 | -------------------------------------------------------------------------------- /resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-ktor-rest-api" 2 | -------------------------------------------------------------------------------- /src/Application.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude 4 | import com.fasterxml.jackson.databind.SerializationFeature 5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 6 | import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException 7 | import com.selimatasoy.di.applicationModule 8 | import com.selimatasoy.errors.GenericServerError 9 | import com.selimatasoy.features.authentication.di.authenticationModule 10 | import com.selimatasoy.features.healthcheck.di.healthCheckModule 11 | import com.selimatasoy.features.starwars.di.starWarsModule 12 | import com.selimatasoy.jwt.JwtManager 13 | import io.ktor.http.* 14 | import io.ktor.serialization.jackson.* 15 | import io.ktor.server.application.* 16 | import io.ktor.server.auth.* 17 | import io.ktor.server.auth.jwt.* 18 | import io.ktor.server.plugins.callloging.* 19 | import io.ktor.server.plugins.contentnegotiation.* 20 | import io.ktor.server.plugins.statuspages.* 21 | import io.ktor.server.request.* 22 | import io.ktor.server.response.* 23 | import org.koin.java.KoinJavaComponent.inject 24 | import org.koin.ktor.plugin.Koin 25 | import org.koin.logger.SLF4JLogger 26 | import org.slf4j.event.Level 27 | import java.text.DateFormat 28 | 29 | fun main(args: Array): Unit = io.ktor.server.tomcat.EngineMain.main(args) 30 | 31 | @Suppress("unused") // Referenced in application.conf 32 | fun Application.module(testing: Boolean = false) { 33 | 34 | install(Koin) { 35 | SLF4JLogger() 36 | modules(applicationModule, authenticationModule, healthCheckModule, starWarsModule) 37 | } 38 | 39 | val jwtManager: JwtManager by inject(JwtManager::class.java) 40 | 41 | install(Authentication) { 42 | jwt { 43 | verifier(jwtManager.getVerifier()) 44 | validate { 45 | UserIdPrincipal(it.payload.getClaim("email").asString()) 46 | } 47 | } 48 | } 49 | 50 | install(ContentNegotiation) { 51 | jackson { 52 | enable(SerializationFeature.INDENT_OUTPUT) 53 | disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 54 | setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL) 55 | registerModule(JavaTimeModule()) 56 | dateFormat = DateFormat.getDateInstance() 57 | } 58 | } 59 | 60 | install(StatusPages) { 61 | exception { call, cause -> 62 | call.response.status(HttpStatusCode.InternalServerError) 63 | call.respond(GenericServerError(500, cause.message.toString())) 64 | throw cause 65 | } 66 | exception { call, cause -> 67 | call.respond(HttpStatusCode.BadRequest) 68 | throw cause 69 | } 70 | } 71 | 72 | install(CallLogging) { 73 | level = Level.INFO 74 | filter { call -> call.request.path().startsWith("/") } 75 | } 76 | 77 | routes() 78 | } -------------------------------------------------------------------------------- /src/RouteManager.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy 2 | 3 | import com.selimatasoy.features.authentication.routes.authenticationRoutes 4 | import com.selimatasoy.features.healthcheck.routes.healthCheckRoutes 5 | import com.selimatasoy.features.starwars.routes.starWarsRoutes 6 | import io.ktor.server.application.* 7 | 8 | fun Application.routes() { 9 | authenticationRoutes() 10 | healthCheckRoutes() 11 | starWarsRoutes() 12 | } -------------------------------------------------------------------------------- /src/di/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.di 2 | 3 | import com.selimatasoy.httpclient.HttpService 4 | import com.selimatasoy.httpclient.HttpServiceImpl 5 | import com.selimatasoy.jwt.JwtManager 6 | import com.selimatasoy.jwt.JwtManagerImpl 7 | import com.typesafe.config.ConfigFactory 8 | import org.koin.core.qualifier.named 9 | import org.koin.dsl.bind 10 | import org.koin.dsl.module 11 | 12 | val applicationModule = module { 13 | single { JwtManagerImpl(get(qualifier = named("jwtSecretProperty"))) } bind JwtManager::class 14 | single(named("jwtSecretProperty")) { ConfigFactory.load().getString("jwt.secret").toString() } 15 | factory { HttpServiceImpl() } bind HttpService::class 16 | } -------------------------------------------------------------------------------- /src/errors/GenericServerError.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.errors 2 | 3 | data class GenericServerError (val httpStatus: Int, val message:String) -------------------------------------------------------------------------------- /src/extensions/ApplicationCallExt.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.extensions 2 | 3 | import io.ktor.server.application.* 4 | 5 | fun ApplicationCall.getAuthorizationTokenWithoutBearer(): String? { 6 | return this.request.headers["Authorization"]?.substring(7) 7 | } -------------------------------------------------------------------------------- /src/extensions/DatabaseExt.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.extensions 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import org.jetbrains.exposed.sql.Database 5 | 6 | fun Database.Companion.connectToExampleDatabase() { 7 | val conf = ConfigFactory.load() 8 | connect( 9 | url = conf.getString("database.exampleDatabaseUrl"), 10 | driver = conf.getString("database.exampleDatabaseDriver"), 11 | user = conf.getString("database.exampleDatabaseUser"), 12 | password = conf.getString("database.exampleDatabasePassword") 13 | ) 14 | } -------------------------------------------------------------------------------- /src/features/authentication/dao/AuthenticationDao.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.dao 2 | 3 | import com.selimatasoy.features.authentication.model.LoginRequestDto 4 | import com.selimatasoy.features.authentication.model.UserInfoDto 5 | 6 | interface AuthenticationDao { 7 | fun login(request: LoginRequestDto): Boolean 8 | fun getUserInfo(email:String): UserInfoDto 9 | fun createUser(userInfoDto: UserInfoDto) 10 | } -------------------------------------------------------------------------------- /src/features/authentication/dao/AuthenticationDaoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.dao 2 | 3 | import com.selimatasoy.extensions.connectToExampleDatabase 4 | import com.selimatasoy.features.authentication.dao.entity.User 5 | import com.selimatasoy.features.authentication.dao.mapper.AuthenticationMapper 6 | import com.selimatasoy.features.authentication.model.LoginRequestDto 7 | import com.selimatasoy.features.authentication.model.UserInfoDto 8 | import org.jetbrains.exposed.sql.* 9 | import org.jetbrains.exposed.sql.transactions.transaction 10 | 11 | class AuthenticationDaoImpl(private val mapper: AuthenticationMapper) : AuthenticationDao { 12 | 13 | override fun login(request: LoginRequestDto): Boolean { 14 | Database.connectToExampleDatabase() 15 | 16 | val count: Long = transaction { 17 | addLogger(StdOutSqlLogger) 18 | return@transaction User.select { User.email eq request.email }.count() 19 | } 20 | return count.toInt() == 1 21 | } 22 | 23 | override fun getUserInfo(email:String): UserInfoDto { 24 | Database.connectToExampleDatabase() 25 | 26 | val userInfo = transaction { 27 | addLogger(StdOutSqlLogger) 28 | return@transaction mapper.fromUserDaoToUserInfo(User.select { User.email eq email }.single()) 29 | } 30 | return userInfo 31 | } 32 | 33 | override fun createUser(userInfoDto: UserInfoDto) { 34 | Database.connectToExampleDatabase() 35 | 36 | transaction { 37 | addLogger(StdOutSqlLogger) 38 | SchemaUtils.create(User) 39 | User.insert { 40 | it[name] = userInfoDto.name 41 | it[surname] = userInfoDto.surname 42 | it[email] = userInfoDto.email 43 | it[birthDate] = userInfoDto.birthDate 44 | it[password] = userInfoDto.password!! 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/features/authentication/dao/entity/User.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.dao.entity 2 | 3 | import org.jetbrains.exposed.sql.Table 4 | import org.jetbrains.exposed.sql.javatime.date 5 | 6 | object User : Table("user") { 7 | val id = long("id").autoIncrement().uniqueIndex() 8 | val name = varchar("Name", 50) 9 | val surname = varchar("Surname", 50) 10 | val birthDate = date("birth_date") 11 | val email = varchar("Email", 50) 12 | val password = varchar("Password",20) 13 | 14 | override val primaryKey = PrimaryKey(id, email) 15 | } -------------------------------------------------------------------------------- /src/features/authentication/dao/mapper/AuthenticationMapper.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.dao.mapper 2 | 3 | import com.selimatasoy.features.authentication.model.UserInfoDto 4 | import org.jetbrains.exposed.sql.ResultRow 5 | 6 | interface AuthenticationMapper { 7 | fun fromUserDaoToUserInfo(resultRow: ResultRow): UserInfoDto 8 | } -------------------------------------------------------------------------------- /src/features/authentication/dao/mapper/AuthenticationMapperImpl.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.dao.mapper 2 | 3 | import com.selimatasoy.features.authentication.dao.entity.User 4 | import com.selimatasoy.features.authentication.model.UserInfoDto 5 | import org.jetbrains.exposed.sql.ResultRow 6 | 7 | class AuthenticationMapperImpl : AuthenticationMapper { 8 | 9 | override fun fromUserDaoToUserInfo(resultRow: ResultRow) = UserInfoDto( 10 | email = resultRow[User.email], 11 | name = resultRow[User.name], 12 | surname = resultRow[User.surname], 13 | birthDate = resultRow[User.birthDate] 14 | ) 15 | 16 | } -------------------------------------------------------------------------------- /src/features/authentication/data/AuthenticationData.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.data 2 | 3 | import com.selimatasoy.features.authentication.model.LoginRequestDto 4 | import com.selimatasoy.features.authentication.model.UserInfoDto 5 | 6 | interface AuthenticationData { 7 | fun login(request: LoginRequestDto): String 8 | fun getUserInfo(email:String): UserInfoDto 9 | fun createUser(userInfoDto: UserInfoDto) 10 | } -------------------------------------------------------------------------------- /src/features/authentication/data/AuthenticationDataImpl.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.data 2 | 3 | import com.selimatasoy.features.authentication.dao.AuthenticationDao 4 | import com.selimatasoy.features.authentication.model.LoginRequestDto 5 | import com.selimatasoy.features.authentication.model.UserInfoDto 6 | import com.selimatasoy.jwt.JwtManager 7 | 8 | class AuthenticationDataImpl(private val authenticationDao: AuthenticationDao, private val jwtManager: JwtManager) : 9 | AuthenticationData { 10 | override fun login(request: LoginRequestDto): String { 11 | if (authenticationDao.login(request)) { 12 | return jwtManager.generateToken(request) 13 | } else { 14 | throw Exception("There is no such user") 15 | } 16 | } 17 | 18 | override fun getUserInfo(email: String): UserInfoDto { 19 | return authenticationDao.getUserInfo(email).apply { password = null } 20 | } 21 | 22 | override fun createUser(userInfoDto: UserInfoDto) { 23 | return authenticationDao.createUser(userInfoDto) 24 | } 25 | } -------------------------------------------------------------------------------- /src/features/authentication/di/AuthenticationModule.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.di 2 | 3 | import com.selimatasoy.features.authentication.dao.AuthenticationDao 4 | import com.selimatasoy.features.authentication.dao.AuthenticationDaoImpl 5 | import com.selimatasoy.features.authentication.dao.mapper.AuthenticationMapper 6 | import com.selimatasoy.features.authentication.dao.mapper.AuthenticationMapperImpl 7 | import com.selimatasoy.features.authentication.data.AuthenticationData 8 | import com.selimatasoy.features.authentication.data.AuthenticationDataImpl 9 | import org.koin.dsl.bind 10 | import org.koin.dsl.module 11 | 12 | val authenticationModule = module { 13 | single { AuthenticationDaoImpl(get()) } bind AuthenticationDao::class 14 | single { AuthenticationDataImpl(get(), get()) } bind AuthenticationData::class 15 | factory { AuthenticationMapperImpl() } bind AuthenticationMapper::class 16 | } -------------------------------------------------------------------------------- /src/features/authentication/model/LoginRequestDto.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.model 2 | 3 | data class LoginRequestDto(val email: String, val password: String) -------------------------------------------------------------------------------- /src/features/authentication/model/UserInfoDto.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.model 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat 4 | import java.time.LocalDate 5 | 6 | data class UserInfoDto( 7 | val name: String, 8 | val surname: String, 9 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd.MM.yyyy") val birthDate: LocalDate, 10 | val email: String, 11 | val id: Long? = null, 12 | var password: String? = null, 13 | ) -------------------------------------------------------------------------------- /src/features/authentication/routes/AuthenticationRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.routes 2 | 3 | import com.selimatasoy.features.authentication.routes.createUser.createUser 4 | import com.selimatasoy.features.authentication.routes.loginUser.loginUser 5 | import com.selimatasoy.features.authentication.routes.userInfo.userInfo 6 | import io.ktor.server.application.* 7 | import io.ktor.server.auth.* 8 | import io.ktor.server.routing.* 9 | 10 | fun Application.authenticationRoutes() { 11 | routing { 12 | loginUser() 13 | authenticate { 14 | userInfo() 15 | } 16 | createUser() 17 | } 18 | } -------------------------------------------------------------------------------- /src/features/authentication/routes/createUser/CreateUser.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.routes.createUser 2 | 3 | import com.selimatasoy.features.authentication.data.AuthenticationData 4 | import com.selimatasoy.features.authentication.model.UserInfoDto 5 | import io.ktor.http.* 6 | import io.ktor.server.application.* 7 | import io.ktor.server.request.* 8 | import io.ktor.server.response.* 9 | import io.ktor.server.routing.* 10 | import org.koin.java.KoinJavaComponent.inject 11 | 12 | fun Route.createUser() { 13 | val authenticationData: AuthenticationData by inject(AuthenticationData::class.java) 14 | post("/public-api/v1/authentication/createUser") { 15 | authenticationData.createUser(call.receive()) 16 | call.respond(HttpStatusCode.OK) 17 | } 18 | } -------------------------------------------------------------------------------- /src/features/authentication/routes/loginUser/LoginUser.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.routes.loginUser 2 | 3 | import com.selimatasoy.features.authentication.data.AuthenticationData 4 | import com.selimatasoy.features.authentication.model.LoginRequestDto 5 | import io.ktor.server.application.* 6 | import io.ktor.server.request.* 7 | import io.ktor.server.response.* 8 | import io.ktor.server.routing.* 9 | import org.koin.java.KoinJavaComponent.inject 10 | 11 | fun Route.loginUser() { 12 | val authenticationData: AuthenticationData by inject(AuthenticationData::class.java) 13 | post("/public-api/v1/authentication/login") { 14 | val request = call.receive() 15 | call.respond(mapOf("token" to authenticationData.login(request))) 16 | } 17 | } -------------------------------------------------------------------------------- /src/features/authentication/routes/userInfo/UserInfo.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.authentication.routes.userInfo 2 | 3 | import com.selimatasoy.extensions.getAuthorizationTokenWithoutBearer 4 | import com.selimatasoy.features.authentication.data.AuthenticationData 5 | import com.selimatasoy.jwt.JwtManager 6 | import io.ktor.server.application.* 7 | import io.ktor.server.response.* 8 | import io.ktor.server.routing.* 9 | import org.koin.java.KoinJavaComponent.inject 10 | 11 | fun Route.userInfo() { 12 | val authenticationData: AuthenticationData by inject(AuthenticationData::class.java) 13 | val jwtManager: JwtManager by inject(JwtManager::class.java) 14 | get("/api/v1/authentication/userInfo") { 15 | call.respond(authenticationData.getUserInfo(jwtManager.getUsernameFromToken(call.getAuthorizationTokenWithoutBearer())!!)) 16 | } 17 | } -------------------------------------------------------------------------------- /src/features/healthcheck/data/HealthCheckData.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.healthcheck.data 2 | 3 | interface HealthCheckData { 4 | fun getHealthCheckStatus():String 5 | } -------------------------------------------------------------------------------- /src/features/healthcheck/data/HealthCheckDataImpl.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.healthcheck.data 2 | 3 | class HealthCheckDataImpl : HealthCheckData { 4 | override fun getHealthCheckStatus(): String { 5 | return "Healthy" 6 | } 7 | 8 | } -------------------------------------------------------------------------------- /src/features/healthcheck/di/HealthCheckModule.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.healthcheck.di 2 | 3 | import com.selimatasoy.features.healthcheck.data.HealthCheckDataImpl 4 | import com.selimatasoy.features.healthcheck.data.HealthCheckData 5 | import org.koin.dsl.bind 6 | import org.koin.dsl.module 7 | 8 | val healthCheckModule = module { 9 | single { HealthCheckDataImpl() } bind HealthCheckData::class 10 | } -------------------------------------------------------------------------------- /src/features/healthcheck/routes/HealthCheckRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.healthcheck.routes 2 | 3 | import com.selimatasoy.features.healthcheck.routes.gethealthcheck.getHealthCheck 4 | import io.ktor.server.application.* 5 | import io.ktor.server.routing.* 6 | 7 | fun Application.healthCheckRoutes() { 8 | routing { 9 | getHealthCheck() 10 | } 11 | } -------------------------------------------------------------------------------- /src/features/healthcheck/routes/gethealthcheck/GetHealthCheck.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.healthcheck.routes.gethealthcheck 2 | 3 | import com.selimatasoy.features.healthcheck.data.HealthCheckData 4 | import io.ktor.server.application.* 5 | import io.ktor.server.response.* 6 | import io.ktor.server.routing.* 7 | import org.koin.java.KoinJavaComponent.inject 8 | 9 | fun Route.getHealthCheck() { 10 | val service: HealthCheckData by inject(HealthCheckData::class.java) 11 | get("/public-api/v1/healthCheck") { 12 | call.respond(object { 13 | val response = service.getHealthCheckStatus() 14 | }) 15 | } 16 | } -------------------------------------------------------------------------------- /src/features/starwars/data/StarWarsData.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.starwars.data 2 | 3 | import com.selimatasoy.features.starwars.model.Movie 4 | 5 | interface StarWarsData { 6 | suspend fun getMovie(): Movie 7 | } -------------------------------------------------------------------------------- /src/features/starwars/data/StarWarsDataImpl.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.starwars.data 2 | 3 | import com.selimatasoy.features.starwars.model.Movie 4 | import com.selimatasoy.features.starwars.remote.StarWarsRemote 5 | 6 | class StarWarsDataImpl(private val remote: StarWarsRemote) : StarWarsData { 7 | override suspend fun getMovie(): Movie { 8 | return remote.getMovie() 9 | } 10 | } -------------------------------------------------------------------------------- /src/features/starwars/di/StarWarsModule.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.starwars.di 2 | 3 | import com.selimatasoy.features.starwars.data.StarWarsData 4 | import com.selimatasoy.features.starwars.data.StarWarsDataImpl 5 | import com.selimatasoy.features.starwars.remote.StarWarsRemote 6 | import com.selimatasoy.features.starwars.remote.StarWarsRemoteImpl 7 | import org.koin.dsl.bind 8 | import org.koin.dsl.module 9 | 10 | val starWarsModule = module { 11 | single { StarWarsRemoteImpl(get()) } bind StarWarsRemote::class 12 | single { StarWarsDataImpl(get()) } bind StarWarsData::class 13 | } -------------------------------------------------------------------------------- /src/features/starwars/model/Movie.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.starwars.model 2 | 3 | data class Movie( 4 | val title: String, 5 | val episode_id: Int, 6 | val opening_crawl: String, 7 | val director: String, 8 | val producer: String, 9 | val release_date: String, 10 | val characters: List, 11 | val planets: List, 12 | val starships: List, 13 | val vehicles: List, 14 | val species: List, 15 | val created: String, 16 | val edited: String, 17 | val url: String 18 | ) { 19 | } -------------------------------------------------------------------------------- /src/features/starwars/remote/StarWarsRemote.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.starwars.remote 2 | 3 | import com.selimatasoy.features.starwars.model.Movie 4 | 5 | interface StarWarsRemote { 6 | suspend fun getMovie(): Movie 7 | } -------------------------------------------------------------------------------- /src/features/starwars/remote/StarWarsRemoteImpl.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.starwars.remote 2 | 3 | import com.selimatasoy.features.starwars.model.Movie 4 | import com.selimatasoy.httpclient.HttpServiceImpl 5 | import io.ktor.client.call.* 6 | import io.ktor.client.request.* 7 | 8 | class StarWarsRemoteImpl(private val httpService: HttpServiceImpl) : StarWarsRemote { 9 | override suspend fun getMovie(): Movie { 10 | return httpService.getClient().get("https://swapi.dev/api/films/1/").body() 11 | } 12 | } -------------------------------------------------------------------------------- /src/features/starwars/routes/StarWarsRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.starwars.routes 2 | 3 | import com.selimatasoy.features.starwars.routes.getmovie.getMovie 4 | import io.ktor.server.application.* 5 | import io.ktor.server.auth.* 6 | import io.ktor.server.routing.* 7 | 8 | fun Application.starWarsRoutes() { 9 | routing { 10 | authenticate { 11 | getMovie() 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/features/starwars/routes/getmovie/GetMovie.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.features.starwars.routes.getmovie 2 | 3 | import com.selimatasoy.features.starwars.data.StarWarsData 4 | import io.ktor.server.application.* 5 | import io.ktor.server.response.* 6 | import io.ktor.server.routing.* 7 | import org.koin.java.KoinJavaComponent.inject 8 | 9 | fun Route.getMovie() { 10 | val starWarsData: StarWarsData by inject(StarWarsData::class.java) 11 | get("/api/v1/star-wars/movie") { 12 | call.respond(starWarsData.getMovie()) 13 | } 14 | } -------------------------------------------------------------------------------- /src/httpclient/HttpService.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.httpclient 2 | 3 | import io.ktor.client.* 4 | 5 | interface HttpService { 6 | fun getClient(): HttpClient 7 | } -------------------------------------------------------------------------------- /src/httpclient/HttpServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.httpclient 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.okhttp.* 5 | import io.ktor.client.plugins.* 6 | import io.ktor.client.plugins.contentnegotiation.* 7 | import io.ktor.client.plugins.logging.* 8 | import io.ktor.client.request.* 9 | import io.ktor.http.* 10 | import io.ktor.serialization.gson.* 11 | import java.text.DateFormat 12 | 13 | class HttpServiceImpl : HttpService { 14 | 15 | private val client: HttpClient = HttpClient(OkHttp) { 16 | engine { 17 | config { 18 | followRedirects(true) 19 | } 20 | } 21 | 22 | install(ContentNegotiation) { 23 | gson { 24 | setPrettyPrinting() 25 | setDateFormat(DateFormat.LONG) 26 | } 27 | } 28 | 29 | install(HttpTimeout) { 30 | connectTimeoutMillis = 30000 31 | socketTimeoutMillis = 30000 32 | requestTimeoutMillis = 30000 33 | } 34 | 35 | install(Logging) { 36 | logger = Logger.DEFAULT 37 | level = LogLevel.ALL 38 | } 39 | 40 | install(DefaultRequest) { 41 | header(HttpHeaders.ContentType, ContentType.Application.Json) 42 | header(HttpHeaders.AcceptCharset, Charsets.UTF_8) 43 | header(HttpHeaders.Accept, ContentType.Application.Any) 44 | } 45 | HttpResponseValidator { 46 | handleResponseException { exception -> 47 | val clientException = 48 | exception as? ClientRequestException ?: return@handleResponseException 49 | val exceptionResponse = exception.response 50 | val statusCode = exceptionResponse.status.value 51 | if (exceptionResponse.status != HttpStatusCode.OK) { 52 | throw IllegalStateException("error") 53 | } 54 | } 55 | } 56 | } 57 | 58 | override fun getClient(): HttpClient { 59 | return client 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/jwt/JwtManager.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.jwt 2 | 3 | import com.auth0.jwt.JWTVerifier 4 | import com.selimatasoy.features.authentication.model.LoginRequestDto 5 | import java.util.* 6 | 7 | interface JwtManager { 8 | fun generateToken(loginRequestDto: LoginRequestDto): String 9 | fun getExpiration(): Date 10 | fun getVerifier(): JWTVerifier 11 | fun getUsernameFromToken(token: String?): String? 12 | } -------------------------------------------------------------------------------- /src/jwt/JwtManagerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy.jwt 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.JWTVerifier 5 | import com.auth0.jwt.algorithms.Algorithm 6 | import com.selimatasoy.features.authentication.model.LoginRequestDto 7 | import org.koin.core.component.KoinComponent 8 | import java.util.* 9 | 10 | 11 | class JwtManagerImpl(secret: String) : JwtManager, KoinComponent { 12 | private val validityInMs = 36_000_00 * 1 13 | private val algorithm = Algorithm.HMAC256(secret) 14 | 15 | override fun getVerifier(): JWTVerifier = JWT.require(algorithm).build() 16 | 17 | override fun generateToken(loginRequestDto: LoginRequestDto): String = JWT.create() 18 | .withSubject("Authentication") 19 | .withClaim("email", loginRequestDto.email) 20 | .withExpiresAt(getExpiration()) 21 | .sign(algorithm) 22 | 23 | override fun getExpiration() = Date(System.currentTimeMillis() + validityInMs) 24 | 25 | override fun getUsernameFromToken(token: String?): String? { 26 | return JWT.decode(token).getClaim("email").toString().replace("\"", "") 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /test/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package com.selimatasoy 2 | 3 | import io.ktor.application.* 4 | import io.ktor.response.* 5 | import io.ktor.request.* 6 | import io.ktor.routing.* 7 | import io.ktor.http.* 8 | import io.ktor.auth.* 9 | import io.ktor.gson.* 10 | import io.ktor.features.* 11 | import org.slf4j.event.* 12 | import kotlin.test.* 13 | import io.ktor.server.testing.* 14 | 15 | class ApplicationTest { 16 | @Test 17 | fun testRoot() { 18 | withTestApplication({ module(testing = true) }) { 19 | handleRequest(HttpMethod.Get, "/").apply { 20 | assertEquals(HttpStatusCode.OK, response.status()) 21 | assertEquals("HELLO WORLD!", response.content) 22 | } 23 | } 24 | } 25 | } 26 | --------------------------------------------------------------------------------