5 Commits
Author SHA1 Message Date
schn33fuchs d890255631 Added router for routing requests
1. Duh
2. Also minor bugfix
2026-04-22 21:27:08 +02:00
schn33fuchs 20a0534c76 Added selecting specific ticket by id 2026-04-22 20:58:52 +02:00
schn33fuchs 8b34dac813 Added functions for adding, deleting and showing tickets
As said, functions got implemented. Also minor changes to migration
layout and dependencies
2026-04-22 20:40:28 +02:00
schn33fuchs 1725d2538c Added status Struct 2026-04-22 18:45:02 +02:00
schn33fuchs a9e31e2fdf Implemeted Display trait for Category enum 2026-04-22 18:29:56 +02:00
12 changed files with 246 additions and 20 deletions
+4
View File
@@ -1493,6 +1493,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
"chrono",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
"either", "either",
@@ -1569,6 +1570,7 @@ dependencies = [
"bitflags", "bitflags",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"crc", "crc",
"digest", "digest",
"dotenvy", "dotenvy",
@@ -1610,6 +1612,7 @@ dependencies = [
"base64", "base64",
"bitflags", "bitflags",
"byteorder", "byteorder",
"chrono",
"crc", "crc",
"dotenvy", "dotenvy",
"etcetera", "etcetera",
@@ -1644,6 +1647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [ dependencies = [
"atoi", "atoi",
"chrono",
"flume", "flume",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
+1 -1
View File
@@ -7,7 +7,7 @@ edition = "2024"
axum = "0.8.9" axum = "0.8.9"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls", "chrono"] }
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros"] }
dotenv = "0.15.0" dotenv = "0.15.0"
chrono = { version = "0.4.44", features = ["serde"] } chrono = { version = "0.4.44", features = ["serde"] }
+1
View File
@@ -0,0 +1 @@
pub mod ticket;
+133
View File
@@ -0,0 +1,133 @@
use std::sync::Arc;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use serde_json::json;
use sqlx::query;
use crate::{
AppState,
models::{Ticket, TicketCreateScheme, TicketResponse},
};
pub async fn create_ticket(
State(data): State<Arc<AppState>>,
Json(body): Json<TicketCreateScheme>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let query = query(
r#"INSERT INTO tickets (category, description, betreff, room) VALUES ($1, $2, $3, $4)"#,
)
.bind(body.category.to_string())
.bind(body.description.to_string())
.bind(body.betreff.to_string())
.bind(body.room.to_string())
.execute(&data.db)
.await;
if let Err(err) = query {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("{:?}", err),})),
));
}
let response_status = serde_json::json!({"status": "success"});
Ok(Json(response_status))
}
pub async fn delete_ticket(
Path(id): Path<i32>,
State(data): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let query = sqlx::query(r#"DELETE FROM tickets WHERE id = $1"#)
.bind(id)
.execute(&data.db)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("{:?}", e)})),
)
})?;
if query.rows_affected() == 0 {
let error_response = serde_json::json!({
"status": "error",
"message": format!("Ticket with ID {} not found", id)
});
return Err((StatusCode::NOT_FOUND, Json(error_response)));
}
Ok(StatusCode::NO_CONTENT)
}
pub async fn get_tickets(
State(data): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let tickets =
sqlx::query_as(r#"SELECT * FROM tickets WHERE status <> 'Archived' ORDER BY date DESC"#)
.fetch_all(&data.db)
.await
.map_err(|e| {
let error_response = serde_json::json!({
"status": "error",
"message": format!("Database error: {}", e),
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
})?;
let ticket_response = tickets
.iter()
.map(|ticket| filter_record(&ticket))
.collect::<Vec<TicketResponse>>();
let json_response = serde_json::json!(ticket_response);
Ok(Json(json_response))
}
pub async fn get_ticket_by_id(
Path(id): Path<i32>,
State(data): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let query = sqlx::query_as(r#"SELECT * FROM tickets WHERE id = $1"#)
.bind(id)
.fetch_one(&data.db)
.await;
match query {
Ok(ticket) => {
let ticket_response = serde_json::json!(filter_record(&ticket));
return Ok(Json(ticket_response));
}
Err(sqlx::Error::RowNotFound) => {
let error_response = serde_json::json!({
"status": "fail",
"message": format!("Ticket with ID {} not found", id)
});
return Err((StatusCode::NOT_FOUND, Json(error_response)));
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("{:?}", e)})),
));
}
};
}
fn filter_record(ticket: &Ticket) -> TicketResponse {
TicketResponse {
id: ticket.id.to_owned(),
category: ticket.category.to_owned(),
betreff: ticket.betreff.to_owned(),
description: ticket.description.to_owned(),
room: ticket.room.to_owned(),
status: ticket.status.to_owned(),
date: ticket.date.to_owned(),
user_id: ticket.user_id.to_owned(),
}
}
+11 -6
View File
@@ -1,10 +1,20 @@
#![allow(unused_imports)]
mod handlers;
mod models; mod models;
mod router;
use std::sync::Arc;
use axum::{Router, routing}; use axum::{Router, routing};
use dotenv::dotenv; use dotenv::dotenv;
use models::*; use models::*;
use router::create_router;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{PgPool, postgres::PgPoolOptions}; use sqlx::{PgPool, postgres::PgPoolOptions};
pub struct AppState {
db: PgPool,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
dotenv().ok(); dotenv().ok();
@@ -19,12 +29,7 @@ async fn main() {
std::process::exit(1); std::process::exit(1);
} }
}; };
let app = Router::new().route("/", routing::get(root_handler)); let app = create_router(Arc::new(AppState { db: pool.clone() }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8001").await.unwrap(); let listener = tokio::net::TcpListener::bind("0.0.0.0:8001").await.unwrap();
axum::serve(listener, app).await; axum::serve(listener, app).await;
} }
async fn root_handler() -> &'static str {
"Hello, World"
}
+63 -7
View File
@@ -1,7 +1,10 @@
use serde::{Deserialize, Serialize}; use std::fmt::Display;
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] use serde::{Deserialize, Serialize};
pub enum category { use sqlx::{Decode, prelude::Type};
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Type)]
pub enum Category {
WhiteboardBeamer, WhiteboardBeamer,
Internet, Internet,
IPadKoffer, IPadKoffer,
@@ -10,21 +13,74 @@ pub enum category {
Sonstiges, Sonstiges,
} }
#[derive(Deserialize, Serialize, PartialEq, Debug)] impl Display for Category {
pub struct ticket { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WhiteboardBeamer => write!(f, "Whiteboard Beamer"),
Self::Internet => write!(f, "Internet"),
Self::IPadKoffer => write!(f, "IPad Koffer"),
Self::AppleTV => write!(f, "Apple TV"),
Self::DocuCam => write!(f, "Docu Cam"),
Self::Sonstiges => write!(f, "Sonstiges"),
}
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Type)]
pub enum Status {
ToDo,
InProgress,
Done,
Archived,
}
impl Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ToDo => write!(f, "ToDo"),
Self::InProgress => write!(f, "InProgress"),
Self::Done => write!(f, "Done"),
Self::Archived => write!(f, "Archived"),
}
}
}
#[derive(Deserialize, Serialize, PartialEq, Debug, sqlx::FromRow)]
pub struct Ticket {
pub id: i32, pub id: i32,
pub category: category, pub category: Category,
pub betreff: String, pub betreff: String,
pub description: String, pub description: String,
pub room: i16, pub room: i16,
pub status: Status,
pub date: chrono::NaiveDateTime,
pub user_id: i16,
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub struct TicketResponse {
pub id: i32,
pub category: Category,
pub betreff: String,
pub description: String,
pub room: i16,
pub status: Status,
pub date: chrono::NaiveDateTime, pub date: chrono::NaiveDateTime,
pub user_id: i16, pub user_id: i16,
} }
#[derive(Deserialize, Serialize, PartialEq, Debug)] #[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct user { pub struct User {
pub id: i16, pub id: i16,
pub first_name: String, pub first_name: String,
pub last_name: String, pub last_name: String,
pub is_admin: bool, pub is_admin: bool,
} }
#[derive(Deserialize, Serialize, Debug)]
pub struct TicketCreateScheme {
pub category: Category,
pub betreff: String,
pub description: String,
pub room: i16,
}
+22
View File
@@ -0,0 +1,22 @@
use std::sync::Arc;
use axum::{
Router,
routing::{get, post},
};
use crate::{
AppState,
handlers::ticket::{create_ticket, delete_ticket, get_ticket_by_id, get_tickets},
};
pub fn create_router(state: Arc<AppState>) -> Router {
Router::new()
.route("/api/tickets", get(get_tickets))
.route("/api/tickets/create", post(create_ticket))
.route(
"/api/tickets/{id}",
get(get_ticket_by_id).delete(delete_ticket),
)
.with_state(state)
}
+2
View File
@@ -0,0 +1,2 @@
DROP TYPE category;
DROP TYPE status;
+2
View File
@@ -0,0 +1,2 @@
CREATE TYPE category AS ENUM('Whiteboard Beamer', 'Internet', 'iPad Koffer', 'Apple TV', 'Docu Cam', 'Sonstiges');
CREATE TYPE status AS ENUM('ToDo', 'InProgress', 'Done', 'Archived');
@@ -0,0 +1,2 @@
DROP TABLE tickets;
+4 -5
View File
@@ -1,11 +1,10 @@
CREATE TYPE category AS ENUM('Whiteboard Beamer', 'Internet', 'iPad Koffer', 'Apple TV', 'Docu Cam', 'Sonstiges')
CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
category category NOT NULL DEFAULT 'Sonstiges', category category NOT NULL DEFAULT 'Sonstiges',
betreff VARCHAR(100), betreff VARCHAR(100),
description VARCHAR, description TEXT,
room SMALLINT, room SMALLINT,
date TIMESTAMP, status status NOT NULL DEFAULT 'ToDo',
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
user_id SMALLINT user_id SMALLINT
); );
+1 -1
View File
@@ -1,5 +1,5 @@
CREATE TABLE users ( CREATE TABLE users (
id SMALLINT PRIMARY KEY AUTOINCREMENT, id SMALLINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(30), name VARCHAR(30),
firstname VARCHAR(30), firstname VARCHAR(30),
is_admin BOOLEAN NOT NULL DEFAULT false is_admin BOOLEAN NOT NULL DEFAULT false