💥 Nukes rust and replaces with kotlin

This commit is contained in:
Daniel Svitan
2025-05-11 10:44:14 +02:00
parent 1413ed58b9
commit 1d4db39b2a
26 changed files with 866 additions and 1912 deletions

View File

@@ -1,56 +0,0 @@
use rocket::Response;
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome, Request};
use serde::Serialize;
use serde_json::json;
use std::io::Cursor;
pub struct ApiKey {}
#[derive(Serialize)]
pub struct GenericResponse {
pub message: String,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for ApiKey {
type Error = Response<'r>;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Response<'r>> {
fn is_valid(key: &str) -> bool {
key == dotenv::var("API_KEY").unwrap()
}
match req.headers().get_one("Authorization") {
None => {
let body = json!(GenericResponse {
message: "auth token not found".to_string()
})
.to_string();
Outcome::Error((
Status::Unauthorized,
Response::build()
.status(Status::Unauthorized)
.sized_body(body.len(), Cursor::new(body))
.finalize(),
))
}
Some(key) if is_valid(key) => Outcome::Success(ApiKey {}),
Some(_) => {
let body = json!(GenericResponse {
message: "invalid auth token".to_string()
})
.to_string();
Outcome::Error((
Status::Unauthorized,
Response::build()
.status(Status::Unauthorized)
.sized_body(body.len(), Cursor::new(body))
.finalize(),
))
}
}
}
}

View File

@@ -1,83 +0,0 @@
use rusqlite::{Connection, Error};
#[derive(Debug)]
pub enum ActionKind {
Text,
Script,
}
impl ActionKind {
fn parse(kind: String) -> Result<ActionKind, Error> {
match kind.to_lowercase().as_str() {
"text" => Ok(ActionKind::Text),
"script" => Ok(ActionKind::Script),
_ => Err(Error::QueryReturnedNoRows),
}
}
fn stringify(&self) -> String {
match self {
ActionKind::Text => "text".to_string(),
ActionKind::Script => "script".to_string(),
}
}
}
#[derive(Debug)]
pub struct Action {
id: i32,
name: String,
kind: ActionKind,
source: String,
created_at: String,
updated_at: String,
}
pub struct Conn {
conn: Connection,
}
impl Conn {
pub fn new(db_path: &str) -> Result<Conn, Error> {
let conn = Connection::open(db_path).expect("failed to open database");
conn.execute(
"CREATE TABLE action (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL,
source TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
(), // empty list of parameters.
)?;
Ok(Conn { conn })
}
fn action_by_id(self, id: i32) -> Result<Action, Error> {
let mut query = self.conn.prepare("SELECT * FROM action WHERE id = ?")?;
let mut actions = query.query_map(&[&id], |row| {
Ok(Action {
id: row.get(0)?,
name: row.get(1)?,
kind: ActionKind::parse(row.get(2)?)?,
source: row.get(3)?,
created_at: row.get(4)?,
updated_at: row.get(5)?,
})
})?;
let action = actions.next();
let len = actions.next().unwrap().iter().count();
if len != 0 {
return Err(Error::InvalidQuery);
}
if let None = action {
return Err(Error::QueryReturnedNoRows);
}
action.unwrap()
}
}

View File

@@ -1,28 +0,0 @@
mod db;
mod auth;
use dotenv;
use auth::ApiKey;
#[macro_use]
extern crate rocket;
#[get("/")]
async fn index() -> &'static str {
"Hello World!"
}
#[get("/hi")]
async fn hello(api_key: ApiKey) -> &'static str {
"Hi!"
}
#[launch]
fn rocket() -> _ {
dotenv::dotenv().ok();
let db_path = dotenv::var("DB_PATH").expect("DB_PATH is not set");
dotenv::var("API_KEY").expect("API_KEY is not set");
let db = db::Conn::new(&db_path);
rocket::build().mount("/", routes![index, hello])
}

View File

@@ -0,0 +1,35 @@
package svitan.dev
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.requestvalidation.RequestValidation
import io.ktor.server.plugins.requestvalidation.ValidationResult
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds
import org.jetbrains.exposed.sql.*
import org.slf4j.event.*
fun Application.configureAdministration() {
routing {
route("/") {
install(RateLimiting) {
rateLimiter {
type = TokenBucket::class
capacity = 100
rate = 10.seconds
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
package svitan.dev
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
configureHTTP()
configureSecurity()
configureMonitoring()
configureSerialization()
configureDatabases()
configureAdministration()
configureRouting()
}

View File

@@ -0,0 +1,65 @@
package svitan.dev
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.requestvalidation.RequestValidation
import io.ktor.server.plugins.requestvalidation.ValidationResult
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds
import org.jetbrains.exposed.sql.*
import org.slf4j.event.*
fun Application.configureDatabases() {
val database = Database.connect(
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
user = "root",
driver = "org.h2.Driver",
password = "",
)
val userService = UserService(database)
routing {
// Create user
post("/users") {
val user = call.receive<ExposedUser>()
val id = userService.create(user)
call.respond(HttpStatusCode.Created, id)
}
// Read user
get("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
val user = userService.read(id)
if (user != null) {
call.respond(HttpStatusCode.OK, user)
} else {
call.respond(HttpStatusCode.NotFound)
}
}
// Update user
put("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
val user = call.receive<ExposedUser>()
userService.update(id, user)
call.respond(HttpStatusCode.OK)
}
// Delete user
delete("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
userService.delete(id)
call.respond(HttpStatusCode.OK)
}
}
}

View File

@@ -0,0 +1,34 @@
package svitan.dev
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.requestvalidation.RequestValidation
import io.ktor.server.plugins.requestvalidation.ValidationResult
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds
import org.jetbrains.exposed.sql.*
import org.slf4j.event.*
fun Application.configureHTTP() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
allowHeader("MyCustomHeader")
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
}
install(Compression)
}

View File

@@ -0,0 +1,28 @@
package svitan.dev
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.requestvalidation.RequestValidation
import io.ktor.server.plugins.requestvalidation.ValidationResult
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds
import org.jetbrains.exposed.sql.*
import org.slf4j.event.*
fun Application.configureMonitoring() {
install(CallLogging) {
level = Level.INFO
filter { call -> call.request.path().startsWith("/") }
}
}

View File

@@ -0,0 +1,41 @@
package svitan.dev
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.requestvalidation.RequestValidation
import io.ktor.server.plugins.requestvalidation.ValidationResult
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds
import org.jetbrains.exposed.sql.*
import org.slf4j.event.*
fun Application.configureRouting() {
install(RequestValidation) {
validate<String> { bodyText ->
if (!bodyText.startsWith("Hello"))
ValidationResult.Invalid("Body text should start with 'Hello'")
else ValidationResult.Valid
}
}
install(StatusPages) {
exception<Throwable> { call, cause ->
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
}
}
routing {
get("/") {
call.respondText("Hello World!")
}
}
}

View File

@@ -0,0 +1,58 @@
package svitan.dev
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.requestvalidation.RequestValidation
import io.ktor.server.plugins.requestvalidation.ValidationResult
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds
import org.jetbrains.exposed.sql.*
import org.slf4j.event.*
fun Application.configureSecurity() {
authentication {
basic(name = "myauth1") {
realm = "Ktor Server"
validate { credentials ->
if (credentials.name == credentials.password) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
form(name = "myauth2") {
userParamName = "user"
passwordParamName = "password"
challenge {
/**/
}
}
}
routing {
authenticate("myauth1") {
get("/protected/route/basic") {
val principal = call.principal<UserIdPrincipal>()!!
call.respondText("Hello ${principal.name}")
}
}
authenticate("myauth2") {
get("/protected/route/form") {
val principal = call.principal<UserIdPrincipal>()!!
call.respondText("Hello ${principal.name}")
}
}
}
}

View File

@@ -0,0 +1,29 @@
package svitan.dev
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.requestvalidation.RequestValidation
import io.ktor.server.plugins.requestvalidation.ValidationResult
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds
import org.jetbrains.exposed.sql.*
import org.slf4j.event.*
fun Application.configureSerialization() {
routing {
get("/json/kotlinx-serialization") {
call.respond(mapOf("hello" to "world"))
}
}
}

View File

@@ -0,0 +1,62 @@
package svitan.dev
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction
@Serializable
data class ExposedUser(val name: String, val age: Int)
class UserService(database: Database) {
object Users : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", length = 50)
val age = integer("age")
override val primaryKey = PrimaryKey(id)
}
init {
transaction(database) {
SchemaUtils.create(Users)
}
}
suspend fun create(user: ExposedUser): Int = dbQuery {
Users.insert {
it[name] = user.name
it[age] = user.age
}[Users.id]
}
suspend fun read(id: Int): ExposedUser? {
return dbQuery {
Users.selectAll()
.where { Users.id eq id }
.map { ExposedUser(it[Users.name], it[Users.age]) }
.singleOrNull()
}
}
suspend fun update(id: Int, user: ExposedUser) {
dbQuery {
Users.update({ Users.id eq id }) {
it[name] = user.name
it[age] = user.age
}
}
}
suspend fun delete(id: Int) {
dbQuery {
Users.deleteWhere { Users.id.eq(id) }
}
}
private suspend fun <T> dbQuery(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
}

View File

@@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>

View File

@@ -0,0 +1,21 @@
package svitan.dev
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlin.test.Test
import kotlin.test.assertEquals
class ApplicationTest {
@Test
fun testRoot() = testApplication {
application {
module()
}
client.get("/").apply {
assertEquals(HttpStatusCode.OK, status)
}
}
}