├── .gitignore ├── Procfile ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── resources ├── application.conf └── logback.xml ├── settings.gradle ├── src ├── Application.kt ├── data │ ├── model │ │ ├── LoginRequest.kt │ │ ├── Note.kt │ │ ├── RegisterRequest.kt │ │ └── User.kt │ └── table │ │ ├── NoteEntity.kt │ │ └── UserEntity.kt ├── helpers │ └── ConnectionDataBase.kt ├── repositories │ ├── NoteRepository.kt │ └── UserRepository.kt ├── routes │ ├── NoteRoute.kt │ └── UserRoute.kt └── utils │ ├── MyResponse.kt │ └── TokenManager.kt └── test └── ApplicationTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /out 4 | /build 5 | *.iml 6 | *.ipr 7 | *.iws 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./build/install/example/bin/example 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ServerNoteApp 2 | # this is server app using ktor framework setup with kotlin language 3 | 4 | 5 | ## Built With 6 | 7 | * [Kotlin](https://kotlinlang.org) - As a programming language. 8 | * [Coroutines](https://developer.android.com/kotlin/coroutines) - For multithreading while handling requests to the server and local database. 9 | * [Routing](https://ktor.io/docs/routing-in-ktor.html) - Routing is the core Ktor plugin (formerly known as feature) for handling incoming requests in a server application. When the client makes a request to a specific URL 10 | * [kotlinx.serialization](https://kotlinlang.org) - https://ktor.io/docs/kotlin-serialization.html 11 | * [JSON Web Tokens](https://kotlinlang.org) - JSON Web Token is an open standard that defines a way for securely transmitting information between parties as a JSON object 12 | * [Clean Architecture](https://www.raywenderlich.com/3595916-clean-architecture-tutorial-for-android-getting-started) - Applying Clean Architecture and Solid Principles to build a robust, maintainable, and testable application. 13 | 14 | -------------------------------------------------------------------------------- /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 'org.jetbrains.kotlin.jvm' version "$kotlin_version" 13 | id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" 14 | id 'application' 15 | } 16 | group 'com.example' 17 | version '0.0.1' 18 | mainClassName = "io.ktor.server.netty.EngineMain" 19 | 20 | sourceSets { 21 | main.kotlin.srcDirs = main.java.srcDirs = ['src'] 22 | test.kotlin.srcDirs = test.java.srcDirs = ['test'] 23 | main.resources.srcDirs = ['resources'] 24 | test.resources.srcDirs = ['testresources'] 25 | } 26 | 27 | 28 | repositories { 29 | mavenLocal() 30 | jcenter() 31 | maven { url 'https://kotlin.bintray.com/ktor' } 32 | mavenCentral() 33 | } 34 | tasks.create("stage"){ 35 | dependsOn("installDist") 36 | } 37 | 38 | dependencies { 39 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 40 | implementation "io.ktor:ktor-server-netty:$ktor_version" 41 | implementation "io.ktor:ktor-locations:$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-sessions:$ktor_version" 45 | implementation "io.ktor:ktor-auth:$ktor_version" 46 | implementation "io.ktor:ktor-auth-jwt:$ktor_version" 47 | implementation "io.ktor:ktor-gson:$ktor_version" 48 | testImplementation "io.ktor:ktor-server-tests:$ktor_version" 49 | 50 | implementation("io.ktor:ktor-serialization:$ktor_version") 51 | implementation("org.mindrot:jbcrypt:0.4") 52 | // ktrom 53 | implementation("org.ktorm:ktorm-core:3.4.1") 54 | implementation("mysql:mysql-connector-java:8.0.27") 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ktor_version=1.6.4 2 | kotlin.code.style=official 3 | kotlin_version=1.6.0 4 | logback_version=1.2.7 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamalragab21/ServerNoteApp/b0c0e5da07e01bd159956bf99c3c4fcfb5cf1be8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8080 4 | port = ${?PORT} 5 | } 6 | application { 7 | modules = [ com.example.ApplicationKt.module ] 8 | } 9 | } 10 | secret = "secret111" 11 | issuer = "http://0.0.0.0:8080/" 12 | audience = "http://0.0.0.0:8080/hello" 13 | realm = "Access to 'hello'" -------------------------------------------------------------------------------- /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 = "example" 2 | -------------------------------------------------------------------------------- /src/Application.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.example.helpers.ConnectionDataBase 4 | import com.example.repositories.NoteRepository 5 | import com.example.repositories.UserRepository 6 | import com.example.routes.noteRoute 7 | import com.example.routes.usersRoute 8 | import com.example.utils.TokenManager 9 | import com.typesafe.config.ConfigFactory 10 | import io.ktor.application.* 11 | import io.ktor.response.* 12 | import io.ktor.routing.* 13 | import io.ktor.http.* 14 | import io.ktor.sessions.* 15 | import io.ktor.auth.* 16 | import io.ktor.auth.jwt.* 17 | import io.ktor.config.* 18 | import io.ktor.gson.* 19 | import io.ktor.features.* 20 | import io.ktor.locations.* 21 | import io.ktor.server.engine.* 22 | import io.ktor.server.netty.* 23 | 24 | fun main(args: Array) { 25 | embeddedServer(Netty, port = 4567, host = "0.0.0.0") { 26 | val config = HoconApplicationConfig(ConfigFactory.load()) 27 | val db = ConnectionDataBase.database 28 | val userRepository = UserRepository(db) 29 | val noteRepository = NoteRepository(db) 30 | val tokenManager = TokenManager(config) 31 | 32 | // after validate send user entity from db 33 | install(Authentication) { 34 | jwt("jwt") { 35 | verifier(tokenManager.verifyJWTToken()) 36 | realm = config.property("realm").getString() 37 | validate { jwtCredential -> 38 | val payload = jwtCredential.payload 39 | val email = payload.getClaim("email").asString() 40 | if (email.isNotEmpty()) { 41 | val user = userRepository.findUserByEmail(email) 42 | user 43 | } else { 44 | null 45 | } 46 | } 47 | } 48 | } 49 | 50 | 51 | install(Sessions) { 52 | cookie("MY_SESSION") { 53 | cookie.extensions["SameSite"] = "lax" 54 | } 55 | } 56 | install(Locations) 57 | 58 | install(ContentNegotiation) { 59 | gson {} 60 | } 61 | 62 | 63 | 64 | 65 | routing { 66 | get("/") { 67 | call.respondText("HELLO WORLD!", contentType = ContentType.Text.Plain) 68 | } 69 | 70 | usersRoute(userRepository, tokenManager) 71 | noteRoute(noteRepository, tokenManager) 72 | } 73 | 74 | }.start(wait = true) 75 | } 76 | 77 | data class MySession(val count: Int = 0) 78 | 79 | -------------------------------------------------------------------------------- /src/data/model/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package com.example.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import java.io.Serial 7 | 8 | 9 | @Serializable 10 | data class LoginRequest( 11 | val email: String, 12 | val password: String 13 | ) -------------------------------------------------------------------------------- /src/data/model/Note.kt: -------------------------------------------------------------------------------- 1 | package com.example.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Note( 7 | val id: Int?, 8 | val title: String, 9 | val subTitle: String, 10 | val dataTime: String, 11 | val imagePath: String?=null, 12 | val note: String, 13 | val color: String="#FFFF", 14 | val webLink: String?=null, 15 | val userId: Int? 16 | ) -------------------------------------------------------------------------------- /src/data/model/RegisterRequest.kt: -------------------------------------------------------------------------------- 1 | package com.example.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RegisterRequest( 7 | val username:String, 8 | val email:String, 9 | val image:String, 10 | val password:String 11 | ) -------------------------------------------------------------------------------- /src/data/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.example.data.model 2 | 3 | import io.ktor.auth.* 4 | import kotlinx.serialization.Serializable 5 | import org.mindrot.jbcrypt.BCrypt 6 | 7 | @Serializable 8 | data class User( 9 | val id:Int?=-1, 10 | val username:String, 11 | val email:String, 12 | val image:String?, 13 | val password:String 14 | ): Principal { 15 | fun hashedPassword(): String { 16 | return BCrypt.hashpw(password, BCrypt.gensalt()) 17 | } 18 | 19 | fun matchPassword(hashPassword:String):Boolean{ 20 | val doesPasswordMatch = BCrypt.checkpw( hashPassword,password) 21 | return doesPasswordMatch 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/data/table/NoteEntity.kt: -------------------------------------------------------------------------------- 1 | package com.example.data.table 2 | 3 | import org.ktorm.schema.Table 4 | import org.ktorm.schema.int 5 | import org.ktorm.schema.varchar 6 | 7 | object NoteEntity:Table("Note") { 8 | 9 | val id = int("id").primaryKey() 10 | val title = varchar("title") 11 | val subTitle = varchar("subTitle") 12 | val dataTime = varchar("dataTime") 13 | val imagePath = varchar("imagePath") 14 | val note = varchar("note") 15 | val color = varchar("color") 16 | val webLink = varchar("webLink") 17 | val userId = int("userId") 18 | } -------------------------------------------------------------------------------- /src/data/table/UserEntity.kt: -------------------------------------------------------------------------------- 1 | package com.example.data.table 2 | 3 | import org.ktorm.schema.Table 4 | import org.ktorm.schema.int 5 | import org.ktorm.schema.varchar 6 | 7 | object UserEntity:Table("User") { 8 | val userId = int("userId").primaryKey() 9 | val username = varchar("username") 10 | val image = varchar("image") 11 | val email = varchar("email") 12 | val haspassord = varchar("haspassord") 13 | } -------------------------------------------------------------------------------- /src/helpers/ConnectionDataBase.kt: -------------------------------------------------------------------------------- 1 | package com.example.helpers 2 | 3 | import org.ktorm.database.Database 4 | 5 | object ConnectionDataBase { 6 | 7 | val database = Database.connect( 8 | 9 | 10 | url = "jdbc:mysql://localhost:3306/notes", 11 | driver = "com.mysql.cj.jdbc.Driver", 12 | user = "root", 13 | password = "Gamal@@2172001" 14 | ) 15 | 16 | } -------------------------------------------------------------------------------- /src/repositories/NoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.repositories 2 | 3 | import com.example.data.model.Note 4 | import com.example.data.table.NoteEntity 5 | import com.example.data.table.UserEntity 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import org.ktorm.database.Database 9 | import org.ktorm.dsl.* 10 | 11 | class NoteRepository(private val db: Database) { 12 | 13 | 14 | suspend fun insertNote(note: Note, userId: Int) = withContext(Dispatchers.IO) { 15 | val result = db.insert(NoteEntity) { 16 | set(it.title, note.title) 17 | set(it.subTitle, note.subTitle) 18 | set(it.note, note.note) 19 | set(it.dataTime, note.dataTime) 20 | set(it.color, note.color) 21 | set(it.userId, userId) 22 | set(it.imagePath, note.imagePath) 23 | set(it.webLink, note.webLink) 24 | } 25 | result 26 | } 27 | 28 | suspend fun updateNote(note: Note, userId: Int) = withContext(Dispatchers.IO) { 29 | val result = db.update(NoteEntity) { 30 | set(it.note, note.note) 31 | set(it.color, note.color) 32 | set(it.imagePath, note.imagePath) 33 | set(it.title, note.title) 34 | set(it.subTitle, note.subTitle) 35 | where { 36 | (it.id eq note.id!!) and (it.userId eq userId) 37 | } 38 | } 39 | result 40 | } 41 | 42 | suspend fun findNoteById(noteId: Int, userId: Int)=withContext(Dispatchers.IO){ 43 | val note = db.from(NoteEntity) 44 | .select() 45 | .where { 46 | (NoteEntity.id eq noteId) and (NoteEntity.userId eq userId) 47 | }.map { 48 | rowToNote(it) 49 | }.firstOrNull() 50 | 51 | note 52 | } 53 | 54 | suspend fun deleteNote(noteId: Int, userId: Int) = withContext(Dispatchers.IO) { 55 | val result = db.delete(NoteEntity) { 56 | (it.id eq noteId) and (it.userId eq userId) 57 | } 58 | result 59 | } 60 | 61 | suspend fun getAllNotes(userId: Int) = withContext(Dispatchers.IO) { 62 | val notes = db.from(NoteEntity).select() 63 | .where { 64 | NoteEntity.userId eq userId 65 | }.mapNotNull { 66 | rowToNote(it) 67 | } 68 | 69 | notes 70 | } 71 | 72 | private fun rowToNote(row: QueryRowSet?): Note? { 73 | return if (row == null) { 74 | null 75 | } else { 76 | Note( 77 | row[NoteEntity.id] ?: -1, 78 | row[NoteEntity.title] ?: "", 79 | row[NoteEntity.subTitle] ?: "", 80 | row[NoteEntity.dataTime] ?: "", 81 | row[NoteEntity.imagePath] ?:"", 82 | row[NoteEntity.note] ?: "", 83 | row[NoteEntity.color] ?:"#333333", 84 | row[NoteEntity.webLink] ?:"", 85 | row[NoteEntity.userId] ?: -1 86 | ) 87 | } 88 | } 89 | 90 | 91 | } -------------------------------------------------------------------------------- /src/repositories/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.repositories 2 | 3 | import com.example.data.model.User 4 | import com.example.data.table.UserEntity 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import org.ktorm.database.Database 8 | import org.ktorm.dsl.* 9 | 10 | 11 | class UserRepository(val db:Database) { 12 | 13 | suspend fun register(user:User) = withContext(Dispatchers.IO){ 14 | val result=db.insert(UserEntity) { 15 | set(it.username, user.username) 16 | set(it.email, user.email) 17 | set(it.haspassord, user.hashedPassword()) 18 | set(it.image, user.image) 19 | } 20 | result 21 | } 22 | 23 | suspend fun findUserByEmail(email:String)=withContext(Dispatchers.IO){ 24 | // this fun check if user email exist or not and if exists return user info 25 | val user = db.from(UserEntity) 26 | .select() 27 | .where { 28 | UserEntity.email eq email 29 | }.map { 30 | rowToUser(it) 31 | }.firstOrNull() 32 | 33 | user 34 | } 35 | 36 | suspend fun findUserById(userId:Int)=withContext(Dispatchers.IO){ 37 | // this fun check if user email exist or not and if exists return user info 38 | val user = db.from(UserEntity) 39 | .select() 40 | .where { 41 | UserEntity.userId eq userId 42 | }.map { 43 | rowToUser(it) 44 | }.firstOrNull() 45 | 46 | user 47 | } 48 | 49 | private fun rowToUser(row:QueryRowSet?):User?{ 50 | return if (row==null){ 51 | null 52 | }else{ 53 | val id = row[UserEntity.userId]?:-1 54 | val email = row[UserEntity.email]?:"" 55 | val username = row[UserEntity.username] ?:"" 56 | val image = row[UserEntity.image] ?:"" 57 | val haspassord = row[UserEntity.haspassord] ?:"" 58 | User(id, username, email, image,haspassord) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/routes/NoteRoute.kt: -------------------------------------------------------------------------------- 1 | package com.example.routes 2 | 3 | import com.example.data.model.Note 4 | import com.example.data.model.User 5 | import com.example.repositories.NoteRepository 6 | import com.example.utils.MyResponse 7 | import com.example.utils.TokenManager 8 | import io.ktor.application.* 9 | import io.ktor.auth.* 10 | import io.ktor.http.* 11 | import io.ktor.request.* 12 | import io.ktor.response.* 13 | import io.ktor.routing.* 14 | 15 | const val NOTES = "$API_VERSION/notes" 16 | const val CREATE_NOTES = "$NOTES/create" 17 | const val UPDATE_NOTES = "$NOTES/update" 18 | const val DELETE_NOTES = "$NOTES/delete" 19 | const val SELECT_NOTE = "$NOTES/{id}" 20 | 21 | //@Location(CREATE_NOTES) 22 | //class NoteCreateRoute 23 | // 24 | //@Location(NOTES) 25 | //class NoteGetRoute 26 | // 27 | //@Location(UPDATE_NOTES) 28 | //class NoteUpdateRoute 29 | // 30 | //@Location(DELETE_NOTES) 31 | //class NoteDeleteRoute 32 | 33 | 34 | fun Route.noteRoute(noteRepository: NoteRepository, tokenManager: TokenManager) { 35 | 36 | authenticate("jwt") { 37 | 38 | // base_url/v1/notes 39 | get(NOTES) { 40 | try { 41 | // get user info from jwt 42 | val user = call.principal()!! 43 | 44 | print("User ${user.toString()}") 45 | // get notes of this user 46 | val notes = noteRepository.getAllNotes(user.id!!) 47 | 48 | call.respond( 49 | HttpStatusCode.OK, 50 | MyResponse( 51 | success = true, 52 | message = "Success ", 53 | data = notes 54 | ) 55 | ) 56 | 57 | } catch (e: Exception) { 58 | call.respond( 59 | HttpStatusCode.OK, 60 | MyResponse( 61 | success = false, 62 | message = e.message ?: "Conflict during get note", 63 | data = null 64 | ) 65 | ) 66 | } 67 | 68 | } 69 | // base_url/v1/notes/create 70 | post(CREATE_NOTES) { 71 | val note:Note = try { 72 | call.receive() 73 | } catch (e: Exception) { 74 | call.respond( 75 | HttpStatusCode.OK, MyResponse( 76 | false, 77 | "Missing Fields", data = null 78 | ) 79 | ) 80 | return@post 81 | } 82 | try { 83 | // get user info from jwt 84 | val user = call.principal()!! 85 | 86 | // insert note 87 | val result = noteRepository.insertNote(note, user.id!!) 88 | // check successfully or note 89 | if (result > 0) { 90 | call.respond( 91 | HttpStatusCode.OK, 92 | MyResponse( 93 | success = true, 94 | message = "Insert Note Successfully", 95 | data = null 96 | ) 97 | ) 98 | return@post 99 | } else { 100 | call.respond( 101 | HttpStatusCode.OK, 102 | MyResponse( 103 | success = false, 104 | message = "Failed insert note.", 105 | data = null 106 | ) 107 | ) 108 | return@post 109 | } 110 | 111 | } catch (e: Exception) { 112 | call.respond( 113 | HttpStatusCode.OK, 114 | MyResponse( 115 | success = false, 116 | message = e.message ?: "Conflict during insert note", 117 | data = null 118 | ) 119 | ) 120 | return@post 121 | } 122 | 123 | } 124 | 125 | // base_url/v1/notes/update 126 | put(UPDATE_NOTES) { 127 | val note = try { 128 | call.receive() 129 | } catch (e: Exception) { 130 | call.respond( 131 | HttpStatusCode.OK, MyResponse( 132 | false, 133 | "Missing Fields", data = null 134 | ) 135 | ) 136 | return@put 137 | } 138 | try { 139 | if (note.id != null) { 140 | // get user info from jwt 141 | val user = call.principal()!! 142 | 143 | // insert note 144 | val result = noteRepository.updateNote(note, user.id!!) 145 | // check successfully or note 146 | if (result > 0) { 147 | call.respond( 148 | HttpStatusCode.OK, 149 | MyResponse( 150 | success = true, 151 | message = "update Note Successfully", 152 | data = note 153 | ) 154 | ) 155 | return@put 156 | } else { 157 | call.respond( 158 | HttpStatusCode.OK, 159 | MyResponse( 160 | success = false, 161 | message = "Failed update note.", 162 | data = null 163 | ) 164 | ) 165 | return@put 166 | } 167 | } else { 168 | call.respond( 169 | HttpStatusCode.OK, 170 | MyResponse( 171 | success = false, 172 | message = "Missing Id of note", 173 | data = null 174 | ) 175 | ) 176 | return@put 177 | } 178 | } catch (e: Exception) { 179 | call.respond( 180 | HttpStatusCode.OK, 181 | MyResponse( 182 | success = false, 183 | message = e.message ?: "Conflict during update note", 184 | data = null 185 | ) 186 | ) 187 | return@put 188 | } 189 | 190 | } 191 | 192 | 193 | // base_url/v1/notes/delete 194 | delete(DELETE_NOTES) { 195 | val id = try { 196 | call.request.queryParameters["id"]!!.toInt() 197 | } catch (e: Exception) { 198 | call.respond( 199 | HttpStatusCode.OK, MyResponse( 200 | false, 201 | "Missing Id Field", data = null 202 | ) 203 | ) 204 | return@delete 205 | } 206 | try { 207 | // get user info from jwt 208 | val user = call.principal()!! 209 | 210 | // insert note 211 | val result = noteRepository.deleteNote(id, user.id!!) 212 | // check successfully or note 213 | if (result > 0) { 214 | call.respond( 215 | HttpStatusCode.OK, 216 | MyResponse( 217 | success = true, 218 | message = "Delete Note Successfully", 219 | data = id 220 | ) 221 | ) 222 | return@delete 223 | } else { 224 | call.respond( 225 | HttpStatusCode.OK, 226 | MyResponse( 227 | success = false, 228 | message = "Failed delete note.", 229 | data = null 230 | ) 231 | ) 232 | return@delete 233 | } 234 | 235 | } catch (e: Exception) { 236 | call.respond( 237 | HttpStatusCode.OK, 238 | MyResponse( 239 | success = false, 240 | message = e.message ?: "Conflict during delete note", 241 | data = null 242 | ) 243 | ) 244 | return@delete 245 | } 246 | 247 | } 248 | 249 | get(SELECT_NOTE) { 250 | val id = try { 251 | call.parameters["id"]?.toInt() ?: -1 252 | } catch (e: Exception) { 253 | call.respond( 254 | HttpStatusCode.OK, MyResponse( 255 | false, 256 | "Missing Id Field", data = null 257 | ) 258 | ) 259 | return@get 260 | } 261 | try { 262 | val user = call.principal()!! 263 | 264 | val note = noteRepository.findNoteById(id, user.id!!) 265 | if (note!=null) { 266 | call.respond( 267 | HttpStatusCode.OK, MyResponse( 268 | true, 269 | "Success get note ", data = note 270 | ) 271 | ) 272 | return@get 273 | }else{ 274 | call.respond( 275 | HttpStatusCode.OK, MyResponse( 276 | true, 277 | "Not found this note" 278 | , data = null 279 | ) 280 | ) 281 | return@get 282 | } 283 | 284 | } catch (e: Exception) { 285 | call.respond( 286 | HttpStatusCode.OK, MyResponse( 287 | false, 288 | "Conflict during get note", data = null 289 | ) 290 | ) 291 | return@get 292 | } 293 | } 294 | 295 | } 296 | 297 | 298 | } -------------------------------------------------------------------------------- /src/routes/UserRoute.kt: -------------------------------------------------------------------------------- 1 | package com.example.routes 2 | 3 | import com.example.data.model.LoginRequest 4 | import com.example.data.model.RegisterRequest 5 | import com.example.data.model.User 6 | import com.example.repositories.UserRepository 7 | import com.example.utils.MyResponse 8 | import com.example.utils.TokenManager 9 | import io.ktor.application.* 10 | import io.ktor.auth.* 11 | import io.ktor.http.* 12 | import io.ktor.locations.* 13 | import io.ktor.request.* 14 | import io.ktor.response.* 15 | import io.ktor.routing.* 16 | 17 | const val API_VERSION = "/v1" 18 | const val USERS = "$API_VERSION/users" 19 | const val REGISTER_REQUEST = "$USERS/register" 20 | const val LOGIN_REQUEST = "$USERS/login" 21 | const val ME_REQUEST = "$USERS/me" 22 | 23 | //@Location(REGISTER_REQUEST) 24 | //class UserRegisterRoute 25 | // 26 | //@Location(LOGIN_REQUEST) 27 | //class UserLoginRoute 28 | 29 | fun Route.usersRoute(userRepository: UserRepository, tokenManager: TokenManager) { 30 | 31 | //base_url/v1/users/register 32 | post(REGISTER_REQUEST) { 33 | // check body request if missing some fields 34 | val registerRequest = try { 35 | call.receive() 36 | } catch (e: Exception) { 37 | call.respond( 38 | HttpStatusCode.OK, 39 | MyResponse( 40 | success = false, 41 | message = "Missing Some Fields", 42 | data = null 43 | ) 44 | ) 45 | return@post 46 | } 47 | 48 | // check if operation connected db successfully 49 | try { 50 | val user = User( 51 | username = registerRequest.username, 52 | email = registerRequest.email, image = registerRequest.image, password = registerRequest.password 53 | ) 54 | // check if email exist or note 55 | if (userRepository.findUserByEmail(user.email) == null) // means not found 56 | { 57 | val result = userRepository.register(user) 58 | // if result >0 it's success else is failed 59 | if (result > 0) { 60 | call.respond( 61 | HttpStatusCode.OK, 62 | MyResponse( 63 | success = true, 64 | message = "Registration Successfully", 65 | data = tokenManager.generateJWTToken(user) 66 | ) 67 | ) 68 | return@post 69 | } else { 70 | call.respond( 71 | HttpStatusCode.OK, 72 | MyResponse( 73 | success = false, 74 | message = "Failed Registration", 75 | data = null 76 | ) 77 | ) 78 | return@post 79 | } 80 | } else { 81 | call.respond( 82 | HttpStatusCode.OK, 83 | MyResponse( 84 | success = false, 85 | message = "User already registration before.", 86 | data = null 87 | ) 88 | ) 89 | return@post 90 | } 91 | 92 | } catch (e: Exception) { 93 | call.respond( 94 | HttpStatusCode.OK, 95 | MyResponse( 96 | success = false, 97 | message = e.message ?: "Failed Registration", 98 | data = null 99 | ) 100 | ) 101 | return@post 102 | } 103 | 104 | } 105 | 106 | //base_url/v1/users/login 107 | post(LOGIN_REQUEST) { 108 | // check body request if missing some fields 109 | val loginRequest = try { 110 | call.receive() 111 | } catch (e: Exception) { 112 | call.respond( 113 | HttpStatusCode.OK, 114 | MyResponse( 115 | success = false, 116 | message = "Missing Some Fields", 117 | data = null 118 | ) 119 | ) 120 | return@post 121 | } 122 | 123 | // check if operation connected db successfully 124 | try { 125 | val user = userRepository.findUserByEmail(loginRequest.email) 126 | 127 | println("User Find ${user.toString()}") 128 | // check if user exist or not 129 | if (user != null) { 130 | // check password after hash pasword 131 | if (user.matchPassword(loginRequest.password)) { 132 | call.respond( 133 | HttpStatusCode.OK, 134 | MyResponse( 135 | success = true, 136 | message = "You are logged in successfully", 137 | data = tokenManager.generateJWTToken(user) 138 | ) 139 | ) 140 | return@post 141 | } else { 142 | call.respond( 143 | HttpStatusCode.OK, 144 | MyResponse( 145 | success = false, 146 | message = "Password Incorrect", 147 | data = null 148 | ) 149 | ) 150 | return@post 151 | } 152 | } else { 153 | call.respond( 154 | HttpStatusCode.OK, 155 | MyResponse( 156 | success = false, 157 | message = "Email is wrong", 158 | data = null 159 | ) 160 | ) 161 | return@post 162 | } 163 | 164 | } catch (e: Exception) { 165 | call.respond( 166 | HttpStatusCode.OK, 167 | MyResponse( 168 | success = false, 169 | message = e.message ?: "Failed Login", 170 | data = null 171 | ) 172 | ) 173 | return@post 174 | } 175 | 176 | 177 | } 178 | 179 | authenticate("jwt") { 180 | get(ME_REQUEST) { 181 | // get user info from jwt 182 | 183 | val user = try{ 184 | call.principal()!! 185 | 186 | }catch (e:Exception){ 187 | call.respond( 188 | HttpStatusCode.OK, 189 | MyResponse( 190 | success = false, 191 | message = e.message ?: "Failed ", 192 | data = null 193 | ) 194 | ) 195 | return@get 196 | } 197 | 198 | call.respond( 199 | HttpStatusCode.OK, 200 | MyResponse( 201 | success = true, 202 | message = "Success", 203 | data = user 204 | ) 205 | ) 206 | return@get 207 | 208 | 209 | } 210 | } 211 | 212 | } -------------------------------------------------------------------------------- /src/utils/MyResponse.kt: -------------------------------------------------------------------------------- 1 | package com.example.utils 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | @Serializable 7 | data class MyResponse( 8 | val success:Boolean=false, 9 | val message:String, 10 | val data:T?=null 11 | ) -------------------------------------------------------------------------------- /src/utils/TokenManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.utils 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.JWTVerifier 5 | import com.auth0.jwt.algorithms.Algorithm 6 | import com.example.data.model.User 7 | import io.ktor.config.* 8 | import java.util.* 9 | 10 | class TokenManager(val config: HoconApplicationConfig) { 11 | val audience = config.property("audience").getString() 12 | val secret = config.property("secret").getString() 13 | val issuer = config.property("issuer").getString() 14 | val expirationDate = System.currentTimeMillis() + 600000; 15 | 16 | fun generateJWTToken(user: User): String { 17 | 18 | val token = JWT.create() 19 | .withAudience(audience) 20 | .withIssuer(issuer) 21 | // .withClaim("email", user.email) 22 | .withClaim("email", user.email) 23 | // .withExpiresAt(Date(expirationDate)) 24 | .sign(Algorithm.HMAC256(secret)) 25 | return token 26 | } 27 | 28 | 29 | 30 | 31 | fun verifyJWTToken(): JWTVerifier { 32 | return JWT.require(Algorithm.HMAC256(secret)) 33 | .withAudience(audience) 34 | .withIssuer(issuer) 35 | .build() 36 | } 37 | } -------------------------------------------------------------------------------- /test/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package com.example 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.sessions.* 9 | import io.ktor.auth.* 10 | import io.ktor.gson.* 11 | import io.ktor.features.* 12 | import io.ktor.server.testing.* 13 | 14 | class ApplicationTest { 15 | // @Test 16 | // fun testRoot() { 17 | // withTestApplication({ module(testing = true) }) { 18 | // handleRequest(HttpMethod.Get, "/").apply { 19 | // assertEquals(HttpStatusCode.OK, response.status()) 20 | // assertEquals("HELLO WORLD!", response.content) 21 | // } 22 | // } 23 | // } 24 | } 25 | --------------------------------------------------------------------------------