diff --git a/src/api/mod.rs b/src/api/mod.rs index bb7b511..a54aaee 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -33,6 +33,8 @@ pub fn api_router() -> Router { .route("/api/sessions/{id}", get(sessions::get_by_id)) .route("/api/sessions/{id}/revoke", post(sessions::revoke_by_id)) // + .route("/api/tags", get(tags::get_all)) + .route("/api/tags", post(tags::create)) .route("/api/tags/{id}", get(tags::get_by_id)) .route("/api/tags/#{name}", get(tags::get_by_name)) } diff --git a/src/api/tags.rs b/src/api/tags.rs index a5241a3..a8e1584 100644 --- a/src/api/tags.rs +++ b/src/api/tags.rs @@ -4,6 +4,7 @@ use axum::{ http::HeaderMap, response::{IntoResponse, Response}, }; +use serde::Deserialize; use uuid::Uuid; use crate::{ @@ -15,6 +16,11 @@ use crate::{ }, }; +pub async fn get_all(headers: HeaderMap) -> Result { + User::authenticate(&headers)?.required()?; + Ok(Json(Tag::get_all()?).into_response()) +} + pub async fn get_by_id( Path(id): Path, headers: HeaderMap, @@ -30,3 +36,15 @@ pub async fn get_by_name( User::authenticate(&headers)?.required()?; Ok(Json(Tag::get_by_name(name)?).into_response()) } + +#[derive(Deserialize)] +pub struct NewTag { + name: TagName, +} +pub async fn create( + headers: HeaderMap, + Json(form): Json, +) -> Result { + User::authenticate(&headers)?.required()?; + Ok(Json(Tag::create(form.name)?).into_response()) +} diff --git a/src/tags.rs b/src/tags.rs index 234b1cc..26304cc 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -6,6 +6,7 @@ use axum::{ }; use rusqlite::{ OptionalExtension, Result as RusqliteResult, ToSql, + ffi::SQLITE_CONSTRAINT_UNIQUE, types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}, }; use serde::{Deserialize, Serialize}; @@ -20,6 +21,17 @@ pub struct Tag { } impl Tag { + pub fn get_all() -> Result, TagError> { + Ok(database::conn()? + .prepare("SELECT id, tagname FROM tags")? + .query_map((), |r| { + Ok(Tag { + id: r.get(0)?, + name: r.get(1)?, + }) + })? + .collect::, _>>()?) + } pub fn get_by_id(id: Uuid) -> Result { let res = database::conn()? .prepare("SELECT tagname FROM tags WHERE id = ?1")? @@ -50,6 +62,13 @@ impl Tag { None => Err(TagError::NoTagWithName(name)), } } + pub fn create(name: TagName) -> Result { + let id = Uuid::now_v7(); + database::conn()? + .prepare("INSERT INTO tags(id, tagname) VALUES (?1, ?2)")? + .execute((id, &name))?; + Ok(Tag { id, name }) + } } #[derive(Debug, thiserror::Error)] @@ -60,11 +79,19 @@ pub enum TagError { NoTagWithId(Uuid), #[error("No tag found with name {0}")] NoTagWithName(TagName), + #[error("A tag with this name already exists")] + TagAlreadyExists, #[error("Database error: {0}")] DatabaseError(#[from] DatabaseError), } impl From for TagError { fn from(error: rusqlite::Error) -> Self { + if let rusqlite::Error::SqliteFailure(e, Some(msg)) = &error + && e.extended_code == SQLITE_CONSTRAINT_UNIQUE + && msg.contains("tagname") + { + return TagError::TagAlreadyExists; + } TagError::DatabaseError(DatabaseError::from(error)) } } @@ -72,6 +99,7 @@ impl IntoResponse for TagError { fn into_response(self) -> Response { match self { Self::DatabaseError(e) => e.into_response(), + Self::TagAlreadyExists => (StatusCode::CONFLICT, self.to_string()).into_response(), Self::TagNameError(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::NoTagWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::NoTagWithName(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), diff --git a/src/users/permissions.rs b/src/users/permissions.rs index 9a5c09b..cb9bee1 100644 --- a/src/users/permissions.rs +++ b/src/users/permissions.rs @@ -11,6 +11,7 @@ pub enum Permission { // All Users have the right to change their own handle ChangeOthersHandles, ManuallyCreateUsers, + CreateTags, } impl User {