Working api.

This commit is contained in:
aminecmi 2022-07-17 15:45:35 +02:00
parent 2d8ad43494
commit 76324145e4
11 changed files with 768 additions and 0 deletions

169
.gitignore vendored Normal file
View File

@ -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

15
README.md Normal file
View File

@ -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

149
pom.xml Normal file
View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>liste-de-courses</artifactId>
<groupId>fr.louveau-amine</groupId>
<version>1.2-SNAPSHOT</version>
<packaging>jar</packaging>
<name>liste-de-courses</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.code.style>official</kotlin.code.style>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<main.class>ServerKt</main.class>
</properties>
<repositories>
<repository>
<id>mavenCentral</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
<repository>
<id>jcenter</id>
<name>jcenter</name>
<url>https://jcenter.bintray.com</url>
</repository>
</repositories>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>1.4.32</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals> <goal>single</goal> </goals>
<configuration>
<archive>
<manifest>
<mainClass>${main.class}</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>1.4.32</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>1.4.32</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>3.13.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.31.1</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-core</artifactId>
<version>0.31.1</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-dao</artifactId>
<version>0.31.1</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-jdbc</artifactId>
<version>0.31.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>2.10.5</version>
</dependency>
</dependencies>
</project>

View File

@ -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)
}
}

View File

@ -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<ItemView>(), ctx)
}
fun update(listId: String, itemId: String, ctx: Context) {
db.updateItem(listId.toInt(), itemId.toInt(), ctx.body<ItemPatchView>(), 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)
}
}

View File

@ -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<ListView>(), 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<ListView>(), ctx)
}
}

195
src/main/kotlin/dao/DB.kt Normal file
View File

@ -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<KeyView>()
val key = Key.find { Keys.value eq k.value }.firstOrNull()
key?.delete() ?: ctx.status(404)
}
}
}

View File

@ -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<Int>) : IntEntity(id) {
companion object : IntEntityClass<Item>(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?)

View File

@ -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<Int>) : IntEntity(id) {
companion object : IntEntityClass<Key>(Keys)
var value by Keys.value
}
fun Key.toView(): KeyView {
return KeyView(this.value)
}
data class KeyView(val value: String)

View File

@ -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<Int>) : IntEntity(id) {
companion object : IntEntityClass<List>(Lists)
var name by Lists.name
}
fun List.toView(): ListView {
return ListView(this.id.value, this.name)
}
fun List.toViewWithItems(items: kotlin.collections.List<Item>): 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<ItemView>)

70
src/main/kotlin/server.kt Normal file
View File

@ -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))
}
}