diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e7bffa1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,169 @@
+# Created by https://www.toptal.com/developers/gitignore/api/kotlin,intellij,maven
+# Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,intellij,maven
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
+
+# Azure Toolkit for IntelliJ plugin
+# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
+.idea/**/azureSettings.xml
+
+### Kotlin ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+# Eclipse m2e generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# End of https://www.toptal.com/developers/gitignore/api/kotlin,intellij,maven,dotenv
+
+
+
+
+.idea/
+data.db
+*.iml
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7ed0302
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# An alternative API to Google Keep
+
+# Authentication
+
+The api use two means of authentication:
+- A configurable `Authorization` that is set in a env variable, and that will only be used to create the second mean of authentication
+- Tokens used to do everything else.
+
+
+## HOW TO USE
+
+- Do a maven install (cli or GUI)
+- Create a env variable `LDC_AUTH` containing the Auth header you want to use to create authentication keys.
+- java -jar liste-de-courses-XXX-jar-with-dependencies.jar
+- The jar is now running on localhost:7000
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..a1e7506
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,149 @@
+
+
+ 4.0.0
+
+ liste-de-courses
+ fr.louveau-amine
+ 1.2-SNAPSHOT
+ jar
+
+ liste-de-courses
+
+
+ UTF-8
+ official
+ 1.8
+ true
+ ServerKt
+
+
+
+
+ mavenCentral
+ https://repo1.maven.org/maven2/
+
+
+ jcenter
+ jcenter
+ https://jcenter.bintray.com
+
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ 1.4.32
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+ maven-failsafe-plugin
+ 2.22.2
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 2.6
+
+
+ make-assembly
+ package
+ single
+
+
+
+ ${main.class}
+
+
+
+ jar-with-dependencies
+
+
+
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit
+ 1.4.32
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ 1.4.32
+
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.30
+
+
+
+ io.javalin
+ javalin
+ 3.13.9
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.10.5
+
+
+ org.xerial
+ sqlite-jdbc
+ 3.31.1
+
+
+
+ org.jetbrains.exposed
+ exposed-core
+ 0.31.1
+
+
+ org.jetbrains.exposed
+ exposed-dao
+ 0.31.1
+
+
+ org.jetbrains.exposed
+ exposed-jdbc
+ 0.31.1
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+ 2.10.5
+
+
+
+
\ No newline at end of file
diff --git a/src/main/kotlin/KeysController.kt b/src/main/kotlin/KeysController.kt
new file mode 100644
index 0000000..0fba188
--- /dev/null
+++ b/src/main/kotlin/KeysController.kt
@@ -0,0 +1,34 @@
+import dao.DB
+import dao.ItemView
+import dao.KeyView
+import io.javalin.apibuilder.CrudHandler
+import io.javalin.http.Context
+
+class KeysController : CrudHandler {
+ private val db: DB = DB()
+
+ override fun create(ctx: Context) {
+ db.createKey(ctx)
+ }
+
+ override fun delete(ctx: Context, resourceId: String) {
+ db.deleteKey(ctx)
+ }
+
+ override fun getAll(ctx: Context) {
+ ctx.status(404)
+ }
+
+ override fun getOne(ctx: Context, resourceId: String) {
+ ctx.status(404)
+ }
+
+ override fun update(ctx: Context, resourceId: String) {
+ ctx.status(404)
+ }
+
+ fun checkKey(ctx: Context) {
+ db.checkKey(ctx)
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/ListItemsController.kt b/src/main/kotlin/ListItemsController.kt
new file mode 100644
index 0000000..4d0bde4
--- /dev/null
+++ b/src/main/kotlin/ListItemsController.kt
@@ -0,0 +1,24 @@
+import dao.DB
+import dao.ItemPatchView
+import dao.ItemView
+import io.javalin.http.Context
+
+class ListItemsController {
+ private val db: DB = DB()
+
+ fun create(listId: String, ctx: Context) {
+ db.createItem(listId.toInt(), ctx.body(), ctx)
+ }
+
+ fun update(listId: String, itemId: String, ctx: Context) {
+ db.updateItem(listId.toInt(), itemId.toInt(), ctx.body(), ctx)
+ }
+
+ fun delete(listId: String, itemId: String, ctx: Context) {
+ db.deleteItem(listId.toInt(), itemId.toInt(), ctx)
+ }
+
+ fun getListItems(ctx: Context, resourceId: String) {
+ db.findListItems(resourceId.toInt(), ctx)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/ListsController.kt b/src/main/kotlin/ListsController.kt
new file mode 100644
index 0000000..2244903
--- /dev/null
+++ b/src/main/kotlin/ListsController.kt
@@ -0,0 +1,29 @@
+import dao.DB
+import dao.ListView
+import io.javalin.apibuilder.CrudHandler
+import io.javalin.http.Context
+
+class ListsController : CrudHandler {
+ private val db: DB = DB()
+
+ override fun create(ctx: Context) {
+ db.createList(ctx.body(), ctx)
+ }
+
+ override fun delete(ctx: Context, resourceId: String) {
+ db.deleteList(resourceId.toInt(), ctx)
+ }
+
+ override fun getAll(ctx: Context) {
+ db.getAllLists(ctx)
+ }
+
+ override fun getOne(ctx: Context, resourceId: String) {
+ db.findList(resourceId.toInt(), ctx)
+ }
+
+ override fun update(ctx: Context, resourceId: String) {
+ db.updateList(resourceId.toInt(), ctx.body(), ctx)
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/dao/DB.kt b/src/main/kotlin/dao/DB.kt
new file mode 100644
index 0000000..d832fce
--- /dev/null
+++ b/src/main/kotlin/dao/DB.kt
@@ -0,0 +1,195 @@
+package dao
+
+import io.javalin.http.Context
+import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class DB {
+
+ /*
+ * Lists
+ */
+ fun createList(c: ListView, ctx: Context) {
+ transaction {
+ var i = List.new {
+ name = c.name
+ }
+ ctx.json(i.toView())
+ }
+
+ }
+
+ fun deleteList(resourceId: Int, ctx: Context) {
+ transaction {
+ val list = List.findById(resourceId)
+
+ if (list != null) {
+ Item.find { Items.list eq list.id.value}.forEach { i -> i.delete() }
+ }
+
+ list?.delete() ?: ctx.status(404)
+ }
+ }
+
+ fun getAllLists(ctx: Context) {
+ transaction {
+ ctx.json(List.all().map { it.toView() })
+ }
+ }
+
+ fun findList(resourceID: Int, ctx: Context) {
+ transaction {
+ val listOpt = List.findById(resourceID)
+ if (listOpt != null) {
+ ctx.json(listOpt.toView())
+ } else {
+ ctx.status(404)
+ }
+ }
+ }
+
+ fun updateList(resourceID: Int, body: ListView, ctx: Context) {
+ transaction {
+ val list = List.findById(resourceID)
+ if (list != null) {
+ if (body.name != null) {
+ list?.name = body.name
+ }
+ ctx.json(list.toView())
+ } else {
+ ctx.status(404)
+ }
+ }
+ }
+
+ /*
+ * Items
+ */
+ fun findListItems(resourceID: Int, ctx: Context) {
+ transaction {
+ val listOpt = List.findById(resourceID)
+ if (listOpt != null) {
+ val items = Item.find { Items.list eq resourceID }
+ .sortedBy { it.position }
+
+ ctx.json(listOpt.toViewWithItems(items))
+ } else {
+ ctx.status(404)
+ }
+ }
+ }
+
+ fun createItem(listId: Int, c: ItemView, ctx: Context) {
+ transaction {
+ val listOpt = List.findById(listId)
+ if (listOpt != null) {
+ var i = Item.new {
+ list = listId
+ content = c.content
+ position = Item.all().count().toInt()
+ checked = false
+ }
+
+ ctx.json(i.toView())
+ } else {
+ ctx.status(404)
+ }
+ }
+
+ }
+
+ fun deleteItem(listId: Int, itemId: Int, ctx: Context) {
+ transaction {
+ val listOpt = List.findById(listId)
+ if (listOpt != null) {
+ val item = Item.find { Items.list eq listId and (Items.id eq itemId) }.limit(1).firstOrNull()
+ if (item != null) {
+ item.delete()
+ var i = 0
+ Item.all()
+ .sortedBy { it.position }
+ .forEach {
+ it.position = i
+ i++
+ }
+ } else {
+ ctx.status(404)
+ }
+ } else {
+ ctx.status(404)
+ }
+ }
+ }
+
+ fun updateItem(listId: Int, itemId: Int, body: ItemPatchView, ctx: Context) {
+ transaction {
+ val listOpt = List.findById(listId)
+ if (listOpt != null) {
+ val item = Item.find { Items.list eq listId and (Items.id eq itemId) }.limit(1).firstOrNull()
+ if (item != null) {
+ var p = body.position
+ if (body.checked != null) {
+ val allITems = Item.find { Items.list eq listId }.sortedBy { it.position }
+ val firstChecked = allITems.firstOrNull { it.checked }
+ p = firstChecked?.position ?: allITems.lastOrNull()?.position
+
+ item?.checked = body.checked
+ }
+ if (p != null) {
+ var i = 0
+ Item.find { Items.list eq listId and (Items.id neq itemId) }
+ .sortedBy { it.position }
+ .forEach {
+ if (i == p) {
+ i++
+ }
+ it.position = i
+ i++
+ }
+ item?.position = p
+ }
+ if (body.content != null) {
+ item?.content = body.content
+ }
+ ctx.json(item.toView())
+ } else {
+ ctx.status(404)
+ }
+ } else {
+ ctx.status(404)
+ }
+ }
+ }
+
+ /*
+ * Api keys
+ */
+ fun createKey(ctx: Context) {
+ transaction {
+ val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+ val k = (1..256)
+ .map { allowedChars.random() }
+ .joinToString("")
+ var key = Key.new {
+ value = k
+ }
+ ctx.json(key.toView())
+ }
+ }
+
+ fun checkKey(ctx: Context) {
+ transaction {
+ val key = ctx.body()
+ if (Key.find { Keys.value eq key }.count() > 0L) ctx.status(301) else ctx.status(404)
+
+ }
+ }
+
+ fun deleteKey(ctx: Context) {
+ transaction {
+ val k = ctx.body()
+ val key = Key.find { Keys.value eq k.value }.firstOrNull()
+ key?.delete() ?: ctx.status(404)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/dao/Items.kt b/src/main/kotlin/dao/Items.kt
new file mode 100644
index 0000000..d9b8aef
--- /dev/null
+++ b/src/main/kotlin/dao/Items.kt
@@ -0,0 +1,31 @@
+package dao
+
+import org.jetbrains.exposed.dao.IntEntity
+import org.jetbrains.exposed.dao.IntEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.dao.id.IntIdTable
+
+object Items : IntIdTable() {
+ val list = integer("list")
+ val content = varchar("content", 256)
+ val position = integer("position")
+ val checked = bool("checked").default(false)
+}
+
+class Item(id: EntityID) : IntEntity(id) {
+ companion object : IntEntityClass- (Items)
+
+ var list by Items.list
+ var content by Items.content
+ var position by Items.position
+ var checked by Items.checked
+
+
+}
+fun Item.toView(): ItemView {
+ return ItemView(this.id.value, this.content, this.checked, this.position)
+}
+
+data class ItemView(val id: Int, val content: String, val checked: Boolean = false, val position: Int?)
+
+data class ItemPatchView(val id: Int, val content: String?, val checked: Boolean?, val position: Int?)
\ No newline at end of file
diff --git a/src/main/kotlin/dao/Keys.kt b/src/main/kotlin/dao/Keys.kt
new file mode 100644
index 0000000..7328680
--- /dev/null
+++ b/src/main/kotlin/dao/Keys.kt
@@ -0,0 +1,23 @@
+package dao
+
+import org.jetbrains.exposed.dao.IntEntity
+import org.jetbrains.exposed.dao.IntEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.dao.id.IntIdTable
+
+object Keys : IntIdTable() {
+ val value = varchar("content", 256)
+}
+
+class Key(id: EntityID) : IntEntity(id) {
+ companion object : IntEntityClass(Keys)
+
+ var value by Keys.value
+
+
+}
+fun Key.toView(): KeyView {
+ return KeyView(this.value)
+}
+
+data class KeyView(val value: String)
\ No newline at end of file
diff --git a/src/main/kotlin/dao/Lists.kt b/src/main/kotlin/dao/Lists.kt
new file mode 100644
index 0000000..d3c3c9e
--- /dev/null
+++ b/src/main/kotlin/dao/Lists.kt
@@ -0,0 +1,29 @@
+package dao
+
+import org.jetbrains.exposed.dao.IntEntity
+import org.jetbrains.exposed.dao.IntEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.dao.id.IntIdTable
+
+object Lists : IntIdTable() {
+ val name = varchar("content", 256)
+}
+
+class List(id: EntityID) : IntEntity(id) {
+ companion object : IntEntityClass
(Lists)
+
+ var name by Lists.name
+
+
+}
+fun List.toView(): ListView {
+ return ListView(this.id.value, this.name)
+}
+
+fun List.toViewWithItems(items: kotlin.collections.List- ): ListViewWithItems {
+ return ListViewWithItems(this.id.value, this.name, items.map { it.toView() })
+}
+
+data class ListView(val id: Int, val name: String)
+
+data class ListViewWithItems(val id: Int, val name: String, val items: kotlin.collections.List)
\ No newline at end of file
diff --git a/src/main/kotlin/server.kt b/src/main/kotlin/server.kt
new file mode 100644
index 0000000..4002370
--- /dev/null
+++ b/src/main/kotlin/server.kt
@@ -0,0 +1,70 @@
+import dao.Items
+import dao.Key
+import dao.Keys
+import dao.Lists
+import io.javalin.Javalin
+import io.javalin.apibuilder.ApiBuilder.*
+import io.javalin.core.security.Role
+import io.javalin.core.security.SecurityUtil.roles
+import org.jetbrains.exposed.sql.Database
+import org.jetbrains.exposed.sql.SchemaUtils
+import org.jetbrains.exposed.sql.StdOutSqlLogger
+import org.jetbrains.exposed.sql.addLogger
+import org.jetbrains.exposed.sql.transactions.transaction
+
+enum class ApiRole : Role { PUBLIC, BASIC_AUTHED, API_AUTHED }
+
+fun main() {
+ val app = Javalin.create {
+ it.accessManager { handler, ctx, permittedRoles ->
+ transaction {
+ val k = ctx.header("X-API-KEY").orEmpty()
+ Key.find {
+ Keys.value eq k
+ }.firstOrNull()
+ val auth = ctx.header("Authorization").orEmpty()
+ val isBasicAuthed = auth == System.getenv("LDC_AUTH")
+ when {
+ permittedRoles.contains(ApiRole.PUBLIC) -> handler.handle(ctx)
+ permittedRoles.contains(ApiRole.BASIC_AUTHED) && isBasicAuthed -> handler.handle(ctx)
+ permittedRoles.contains(ApiRole.API_AUTHED) && k != null -> handler.handle(ctx)
+ else -> ctx.status(401).json("Unauthorized")
+ }
+ }
+ }
+ }.start(7000)
+ Database.connect("jdbc:sqlite:data.db", "org.sqlite.JDBC")
+ transaction {
+ addLogger(StdOutSqlLogger)
+ SchemaUtils.create (Lists)
+ SchemaUtils.create (Items)
+ SchemaUtils.create (Keys)
+ }
+
+ app.routes {
+ path("api") {
+ crud("keys/:key-id", KeysController(), roles(ApiRole.BASIC_AUTHED))
+ crud("lists/:list-id", ListsController(), roles(ApiRole.API_AUTHED))
+ }
+
+ post("/api/keys:check", { ctx ->
+ KeysController().checkKey(ctx)
+ }, roles(ApiRole.PUBLIC))
+
+ get("/api/lists/:list-id/items", { ctx ->
+ ListItemsController().getListItems(ctx, ctx.pathParam("list-id"))
+ }, roles(ApiRole.API_AUTHED))
+
+ post("/api/lists/:list-id/items", { ctx ->
+ ListItemsController().create(ctx.pathParam("list-id"), ctx)
+ }, roles(ApiRole.API_AUTHED))
+
+ delete("/api/lists/:list-id/items/:item-id", { ctx ->
+ ListItemsController().delete(ctx.pathParam("list-id"), ctx.pathParam("item-id"), ctx)
+ }, roles(ApiRole.API_AUTHED))
+
+ patch("/api/lists/:list-id/items/:item-id", { ctx ->
+ ListItemsController().update(ctx.pathParam("list-id"), ctx.pathParam("item-id"), ctx)
+ }, roles(ApiRole.API_AUTHED))
+ }
+}
\ No newline at end of file