├── .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 |
--------------------------------------------------------------------------------