database work
This commit is contained in:
parent
b7a2265e9b
commit
469a3ad339
|
@ -306,10 +306,13 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||||
name = "critch"
|
name = "critch"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"mime",
|
||||||
"poem",
|
"poem",
|
||||||
"ructe",
|
"ructe",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"time",
|
||||||
|
"time-humanize",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
@ -1798,6 +1801,7 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlformat",
|
"sqlformat",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -1882,6 +1886,7 @@ dependencies = [
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
|
@ -1921,6 +1926,7 @@ dependencies = [
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
|
@ -1945,6 +1951,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
@ -2058,6 +2065,12 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-humanize"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e32d019b4f7c100bcd5494e40a27119d45b71fba2b07a4684153129279a4647"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.18"
|
version = "0.2.18"
|
||||||
|
@ -2298,6 +2311,9 @@ name = "uuid"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
|
|
|
@ -10,9 +10,12 @@ build = "src/build.rs"
|
||||||
ructe = { version = "0.17.2", features = ["sass", "mime03"] }
|
ructe = { version = "0.17.2", features = ["sass", "mime03"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
mime = "0.3.17"
|
||||||
poem = { version = "3.1.3", features = ["session"] }
|
poem = { version = "3.1.3", features = ["session"] }
|
||||||
serde = "1.0.215"
|
serde = "1.0.215"
|
||||||
sqlx = { version = "0.8.2", features = ["uuid", "postgres", "runtime-tokio"] }
|
sqlx = { version = "0.8.2", features = ["uuid", "postgres", "runtime-tokio", "time"] }
|
||||||
|
time = "0.3.36"
|
||||||
|
time-humanize = "0.1.3"
|
||||||
tokio = { version = "1.41.1", features = ["full"] }
|
tokio = { version = "1.41.1", features = ["full"] }
|
||||||
toml = { version = "0.8.19", features = ["parse"] }
|
toml = { version = "0.8.19", features = ["parse"] }
|
||||||
uuid = "1.11.0"
|
uuid = { version = "1.11.0", features = ["v4"] }
|
||||||
|
|
|
@ -2,7 +2,8 @@ create extension if not exists "uuid-ossp";
|
||||||
|
|
||||||
create table artists (
|
create table artists (
|
||||||
id integer primary key generated always as identity,
|
id integer primary key generated always as identity,
|
||||||
artist_name varchar(128) not null unique,
|
handle varchar(128) not null unique,
|
||||||
|
name varchar(128),
|
||||||
bio text,
|
bio text,
|
||||||
site varchar(256)
|
site varchar(256)
|
||||||
);
|
);
|
||||||
|
@ -12,6 +13,7 @@ create table artworks (
|
||||||
title varchar(256),
|
title varchar(256),
|
||||||
description text,
|
description text,
|
||||||
url_source varchar(256),
|
url_source varchar(256),
|
||||||
|
created_at timestamp not null default current_timestamp,
|
||||||
artist_id integer not null,
|
artist_id integer not null,
|
||||||
comment_number integer not null default 0,
|
comment_number integer not null default 0,
|
||||||
foreign key (artist_id) references artists(id)
|
foreign key (artist_id) references artists(id)
|
||||||
|
@ -20,24 +22,26 @@ create table artworks (
|
||||||
create table comments (
|
create table comments (
|
||||||
id integer unique not null,
|
id integer unique not null,
|
||||||
text text not null,
|
text text not null,
|
||||||
thread_id integer not null,
|
artwork_id integer not null,
|
||||||
primary key (id, thread_id),
|
created_at timestamp not null default current_timestamp,
|
||||||
foreign key (thread_id) references artworks(id)
|
primary key (id, artwork_id),
|
||||||
|
foreign key (artwork_id) references artworks(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table comment_relations (
|
create table comment_relations (
|
||||||
thread_id integer,
|
artwork_id integer,
|
||||||
foreign key (thread_id) references artworks(id),
|
foreign key (artwork_id) references artworks(id),
|
||||||
in_reply_to_id integer,
|
in_reply_to_id integer,
|
||||||
foreign key (in_reply_to_id) references comments(id),
|
foreign key (in_reply_to_id) references comments(id),
|
||||||
comment_id integer,
|
comment_id integer,
|
||||||
foreign key (comment_id) references comments(id),
|
foreign key (comment_id) references comments(id),
|
||||||
primary key (thread_id, in_reply_to_id, comment_id)
|
primary key (artwork_id, in_reply_to_id, comment_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table files (
|
create table artwork_files (
|
||||||
id uuid primary key default gen_random_uuid(),
|
id uuid primary key default gen_random_uuid(),
|
||||||
alt_text text,
|
alt_text text,
|
||||||
|
extension varchar(16),
|
||||||
artwork_id integer,
|
artwork_id integer,
|
||||||
foreign key (artwork_id) references artworks(id)
|
foreign key (artwork_id) references artworks(id)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
id: Option<usize>,
|
id: Option<i32>,
|
||||||
name: String,
|
pub handle: String,
|
||||||
bio: Option<String>,
|
pub name: Option<String>,
|
||||||
site: Option<String>,
|
pub bio: Option<String>,
|
||||||
|
pub site: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Artist {
|
||||||
|
pub fn id(&self) -> Option<i32> {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
|
use time::{OffsetDateTime, PrimitiveDateTime};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{artist::Artist, comment::Comment, file::File};
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
pub struct Artwork {
|
pub struct Artwork {
|
||||||
/// artwork id
|
/// artwork id
|
||||||
id: Option<usize>,
|
id: Option<i32>,
|
||||||
/// name of the artwork
|
/// name of the artwork
|
||||||
title: Option<String>,
|
pub title: Option<String>,
|
||||||
/// description of the artwork
|
/// description of the artwork
|
||||||
description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// source url of the artwork
|
/// source url of the artwork
|
||||||
url_source: Option<String>,
|
pub url_source: Option<String>,
|
||||||
|
/// artwork creation time
|
||||||
|
created_at: Option<PrimitiveDateTime>,
|
||||||
/// id of the artist
|
/// id of the artist
|
||||||
artist_id: usize,
|
#[sqlx(Flatten)]
|
||||||
|
pub artist: Artist,
|
||||||
/// ids of files
|
/// ids of files
|
||||||
files: Vec<usize>,
|
#[sqlx(Flatten)]
|
||||||
|
pub files: Vec<File>,
|
||||||
|
// /// TODO: comments in thread,
|
||||||
|
// #[sqlx(Flatten)]
|
||||||
|
// comments: Vec<Comment>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,34 @@
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
/// id of the comment in the thread
|
/// id of the comment in the thread
|
||||||
id: Option<usize>,
|
id: Option<i32>,
|
||||||
/// text of the comment
|
/// text of the comment
|
||||||
text: String,
|
pub text: String,
|
||||||
/// thread comment is in
|
/// id of artwork thread comment is in
|
||||||
thread: usize,
|
pub artwork_id: i32,
|
||||||
|
/// comment creation time
|
||||||
|
created_at: Option<OffsetDateTime>,
|
||||||
/// comments that are mentioned by the comment
|
/// comments that are mentioned by the comment
|
||||||
in_reply_to: Vec<usize>,
|
pub in_reply_to_ids: Vec<i32>,
|
||||||
/// comments that mention the comment
|
/// comments that mention the comment
|
||||||
mentioned_by: Vec<usize>,
|
mentioned_by_ids: Vec<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Comment {
|
||||||
|
pub fn id(&self) -> Option<i32> {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn created_at(&self) -> Option<OffsetDateTime> {
|
||||||
|
self.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mentioned_by_ids(&self) -> &Vec<i32> {
|
||||||
|
&self.mentioned_by_ids
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
|
use crate::artist::Artist;
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Artists(Pool<Postgres>);
|
||||||
|
|
||||||
|
impl Artists {
|
||||||
|
pub fn new(pool: Pool<Postgres>) -> Self {
|
||||||
|
Self(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(&self, artist: Artist) -> Result<i32> {
|
||||||
|
let artist_id = sqlx::query!(
|
||||||
|
"insert into artists (handle, name, bio, site) values ($1, $2, $3, $4) returning id",
|
||||||
|
artist.handle,
|
||||||
|
artist.name,
|
||||||
|
artist.bio,
|
||||||
|
artist.site
|
||||||
|
)
|
||||||
|
.fetch_one(&self.0)
|
||||||
|
.await?
|
||||||
|
.id;
|
||||||
|
Ok(artist_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read(&self, id: i32) -> Result<Artist> {
|
||||||
|
Ok(sqlx::query_as("select * from artists where id = $1")
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(&self.0)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_handle(&self, handle: &str) -> Result<Artist> {
|
||||||
|
Ok(sqlx::query_as("select * from artists where handle = $1")
|
||||||
|
.bind(handle)
|
||||||
|
.fetch_one(&self.0)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_all(&self) -> Result<Vec<Artist>> {
|
||||||
|
Ok(sqlx::query_as("select * from artists")
|
||||||
|
.fetch_all(&self.0)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search(&self, query: &str) -> Result<Vec<Artist>> {
|
||||||
|
Ok(
|
||||||
|
sqlx::query_as("select * from artists where handle + name like '%$1%'")
|
||||||
|
.bind(query)
|
||||||
|
.fetch_all(&self.0)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,52 @@
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::artist::Artist;
|
||||||
|
use crate::artwork::Artwork;
|
||||||
|
use crate::file::File;
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use super::Database;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Artworks(Pool<Postgres>);
|
||||||
|
|
||||||
|
impl Artworks {
|
||||||
|
pub fn new(pool: Pool<Postgres>) -> Self {
|
||||||
|
Self(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn downcast(&self) -> Database {
|
||||||
|
Database(self.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(&self, artwork: Artwork) -> Result<i32> {
|
||||||
|
let artist_id = if let Some(artist_id) = artwork.artist.id() {
|
||||||
|
artist_id
|
||||||
|
} else {
|
||||||
|
self.downcast().artists().create(artwork.artist).await?
|
||||||
|
};
|
||||||
|
let artwork_id = sqlx::query!("insert into artworks (title, description, url_source, artist_id) values ($1, $2, $3, $4) returning id", artwork.title, artwork.description, artwork.url_source, artist_id).fetch_one(&self.0).await?.id;
|
||||||
|
for file in artwork.files {
|
||||||
|
sqlx::query!(
|
||||||
|
"insert into artwork_files (id, alt_text, extension, artwork_id) values ($1, $2, $3, $4)",
|
||||||
|
file.id(),
|
||||||
|
file.alt_text,
|
||||||
|
file.extension(),
|
||||||
|
artwork_id
|
||||||
|
)
|
||||||
|
.execute(&self.0)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(artwork_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_all(&self) -> Result<Vec<Artwork>> {
|
||||||
|
// TODO: join comments and files
|
||||||
|
Ok(sqlx::query_as(
|
||||||
|
"select * from artworks left join artists on artworks.artist_id = artists.id left join artwork_files on artworks.id = artwork_files.artwork_id group by artworks.id, artists.id, artwork_files.id",
|
||||||
|
)
|
||||||
|
.fetch_all(&self.0)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
|
use crate::comment::Comment;
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Comments(Pool<Postgres>);
|
||||||
|
|
||||||
|
impl Comments {
|
||||||
|
pub fn new(pool: Pool<Postgres>) -> Self {
|
||||||
|
Self(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(&self, comment: Comment) -> Result<i32> {
|
||||||
|
let comment_id = sqlx::query!(
|
||||||
|
r#"insert into comments (text, artwork_id) values ($1, $2) returning id"#,
|
||||||
|
comment.text,
|
||||||
|
comment.artwork_id
|
||||||
|
)
|
||||||
|
.fetch_one(&self.0)
|
||||||
|
.await?
|
||||||
|
.id;
|
||||||
|
for in_reply_to_id in comment.in_reply_to_ids {
|
||||||
|
sqlx::query!("insert into comment_relations (artwork_id, in_reply_to_id, comment_id) values ($1, $2, $3)", comment.artwork_id, in_reply_to_id, comment_id).execute(&self.0).await?;
|
||||||
|
}
|
||||||
|
Ok(comment_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_all(&self) -> Result<Vec<Comment>> {
|
||||||
|
// TODO: joins to get in_reply_to_ids and mentioned_by_ids
|
||||||
|
let comments: Vec<Comment> = sqlx::query_as("select * from comments")
|
||||||
|
.fetch_all(&self.0)
|
||||||
|
.await?;
|
||||||
|
Ok(comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_thread(&self, artwork_id: i32) -> Result<Vec<Comment>> {
|
||||||
|
Ok(sqlx::query_as("select * from comments")
|
||||||
|
.fetch_all(&self.0)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,11 @@
|
||||||
|
use artists::Artists;
|
||||||
|
use artworks::Artworks;
|
||||||
|
use comments::Comments;
|
||||||
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
|
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
|
||||||
|
|
||||||
|
mod artists;
|
||||||
mod artworks;
|
mod artworks;
|
||||||
|
mod comments;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Database(Pool<Postgres>);
|
pub struct Database(Pool<Postgres>);
|
||||||
|
@ -17,4 +22,16 @@ impl Database {
|
||||||
|
|
||||||
Self(pool)
|
Self(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn artists(&self) -> Artists {
|
||||||
|
Artists::new(self.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn artworks(&self) -> Artworks {
|
||||||
|
Artworks::new(self.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn comments(&self) -> Comments {
|
||||||
|
Comments::new(self.0.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
39
src/error.rs
39
src/error.rs
|
@ -1,22 +1,46 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use poem::{error::ResponseError, http::StatusCode};
|
||||||
|
use sqlx::postgres::PgDatabaseError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
IOError(std::io::Error),
|
IOError(std::io::Error),
|
||||||
TOMLError(toml::de::Error),
|
TOMLError(toml::de::Error),
|
||||||
|
SQLError(String),
|
||||||
|
DatabaseError(sqlx::Error),
|
||||||
|
NotFound,
|
||||||
|
MissingField,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
Error::SQLError(error) => write!(f, "SQL Error: {}", error),
|
||||||
Error::IOError(error) => write!(f, "IO Error: {}", error),
|
Error::IOError(error) => write!(f, "IO Error: {}", error),
|
||||||
Error::TOMLError(error) => write!(f, "TOML deserialization error: {}", error),
|
Error::TOMLError(error) => write!(f, "TOML deserialization error: {}", error),
|
||||||
|
Error::DatabaseError(error) => write!(f, "database error: {}", error),
|
||||||
|
Error::NotFound => write!(f, "not found"),
|
||||||
|
Error::MissingField => write!(f, "missing field in row"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
impl ResponseError for Error {
|
||||||
|
fn status(&self) -> poem::http::StatusCode {
|
||||||
|
match self {
|
||||||
|
Error::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Error::TOMLError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Error::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Error::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
Error::SQLError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Error::MissingField => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for Error {
|
impl From<std::io::Error> for Error {
|
||||||
fn from(e: std::io::Error) -> Self {
|
fn from(e: std::io::Error) -> Self {
|
||||||
Self::IOError(e)
|
Self::IOError(e)
|
||||||
|
@ -28,3 +52,18 @@ impl From<toml::de::Error> for Error {
|
||||||
Self::TOMLError(e)
|
Self::TOMLError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for Error {
|
||||||
|
fn from(e: sqlx::Error) -> Self {
|
||||||
|
match e {
|
||||||
|
sqlx::Error::Database(database_error) => {
|
||||||
|
let error = database_error.downcast::<PgDatabaseError>();
|
||||||
|
match error.code() {
|
||||||
|
code => Error::SQLError(code.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlx::Error::RowNotFound => Error::NotFound,
|
||||||
|
_ => Self::DatabaseError(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
30
src/file.rs
30
src/file.rs
|
@ -2,6 +2,32 @@ use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
id: Option<Uuid>,
|
id: Uuid,
|
||||||
artwork: usize,
|
pub alt_text: String,
|
||||||
|
extension: String,
|
||||||
|
artwork_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
pub fn new(file: std::fs::File, extension: String) -> Self {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
alt_text: String::new(),
|
||||||
|
extension,
|
||||||
|
artwork_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> Uuid {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extension(&self) -> &str {
|
||||||
|
&self.extension
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn artwork_id(&self) -> Option<i32> {
|
||||||
|
self.artwork_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,3 +26,5 @@ impl Critch {
|
||||||
Self { db, config }
|
Self { db, config }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -1,3 +1,5 @@
|
||||||
|
use poem::session::{CookieConfig, CookieSession};
|
||||||
|
use poem::web::cookie::CookieKey;
|
||||||
use poem::{delete, post, put, EndpointExt};
|
use poem::{delete, post, put, EndpointExt};
|
||||||
use poem::{get, listener::TcpListener, Route, Server};
|
use poem::{get, listener::TcpListener, Route, Server};
|
||||||
|
|
||||||
|
@ -10,11 +12,16 @@ async fn main() -> Result<(), std::io::Error> {
|
||||||
let config = Config::from_file("./critch.toml").unwrap();
|
let config = Config::from_file("./critch.toml").unwrap();
|
||||||
let state = Critch::new(config).await;
|
let state = Critch::new(config).await;
|
||||||
|
|
||||||
|
let cookie_config = CookieConfig::private(CookieKey::generate());
|
||||||
|
let cookie_session = CookieSession::new(cookie_config);
|
||||||
|
|
||||||
let app = Route::new()
|
let app = Route::new()
|
||||||
|
.at("/admin", get(routes::admin::get_dashboard))
|
||||||
.at(
|
.at(
|
||||||
"/admin",
|
"/admin/login",
|
||||||
post(routes::admin::login).get(routes::admin::get_login_form),
|
post(routes::admin::login).get(routes::admin::get_login_form),
|
||||||
)
|
)
|
||||||
|
.at("/admin/logout", post(routes::admin::logout))
|
||||||
.at("/", get(routes::artworks::get))
|
.at("/", get(routes::artworks::get))
|
||||||
.at(
|
.at(
|
||||||
"/artworks",
|
"/artworks",
|
||||||
|
@ -40,7 +47,9 @@ async fn main() -> Result<(), std::io::Error> {
|
||||||
"/artworks/:artwork/comments",
|
"/artworks/:artwork/comments",
|
||||||
post(routes::artworks::comments::post).delete(routes::artworks::comments::delete),
|
post(routes::artworks::comments::post).delete(routes::artworks::comments::delete),
|
||||||
)
|
)
|
||||||
.data(state);
|
.catch_all_error(routes::error::error)
|
||||||
|
.data(state)
|
||||||
|
.with(cookie_session);
|
||||||
|
|
||||||
Server::new(TcpListener::bind("0.0.0.0:3000"))
|
Server::new(TcpListener::bind("0.0.0.0:3000"))
|
||||||
.run(app)
|
.run(app)
|
||||||
|
|
|
@ -1,11 +1,50 @@
|
||||||
use poem::handler;
|
use poem::{
|
||||||
|
handler,
|
||||||
|
http::StatusCode,
|
||||||
|
session::Session,
|
||||||
|
web::{Data, Form, Redirect},
|
||||||
|
IntoResponse, Response,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[handler]
|
use crate::{ructe_poem::render, templates, Critch, Result};
|
||||||
pub async fn login() {
|
|
||||||
todo!()
|
#[derive(Deserialize)]
|
||||||
|
struct Login {
|
||||||
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[handler]
|
#[handler]
|
||||||
pub async fn get_login_form() {
|
pub async fn get_dashboard(session: &Session, critch: Data<&Critch>) -> Result<Response> {
|
||||||
todo!()
|
if let Some(true) = session.get("is_admin") {
|
||||||
|
let comments = critch.db.comments().read_all().await?;
|
||||||
|
let artworks = critch.db.artworks().read_all().await?;
|
||||||
|
return Ok(render!(templates::admin_dashboard_html).into_response());
|
||||||
|
} else {
|
||||||
|
return Ok(Redirect::see_other("/admin/login").into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub fn login(session: &Session, data: Data<&Critch>, form: Form<Login>) -> Response {
|
||||||
|
if form.password == data.config.admin_password() {
|
||||||
|
session.set("is_admin", true);
|
||||||
|
return Redirect::see_other("/admin").into_response();
|
||||||
|
} else {
|
||||||
|
return render!(templates::admin_login_html).into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub fn logout(session: &Session) -> Response {
|
||||||
|
session.purge();
|
||||||
|
Redirect::see_other("/").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub fn get_login_form(session: &Session) -> Response {
|
||||||
|
if let Some(true) = session.get("is_admin") {
|
||||||
|
return Redirect::see_other("/admin").into_response();
|
||||||
|
};
|
||||||
|
render!(templates::admin_login_html).into_response()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
use poem::{IntoResponse, Response};
|
||||||
|
|
||||||
|
use crate::{ructe_poem::render, templates};
|
||||||
|
|
||||||
|
pub async fn error(err: poem::Error) -> Response {
|
||||||
|
let status = err.status().to_string();
|
||||||
|
let message = err.to_string();
|
||||||
|
render!(templates::error_html, &status, &message).into_response()
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod artists;
|
pub mod artists;
|
||||||
pub mod artworks;
|
pub mod artworks;
|
||||||
|
pub mod error;
|
||||||
|
|
|
@ -2,11 +2,11 @@ use poem::{http::StatusCode, Body, IntoResponse};
|
||||||
|
|
||||||
macro_rules! render {
|
macro_rules! render {
|
||||||
($template:path) => {{
|
($template:path) => {{
|
||||||
use $crate::axum_ructe::Render;
|
use $crate::ructe_poem::Render;
|
||||||
Render(|o| $template(o))
|
Render(|o| $template(o))
|
||||||
}};
|
}};
|
||||||
($template:path, $($arg:expr),* $(,)*) => {{
|
($template:path, $($arg:expr),* $(,)*) => {{
|
||||||
use $crate::axum_ructe::Render;
|
use $crate::ructe_poem::Render;
|
||||||
Render(move |o| $template(o, $($arg),*))
|
Render(move |o| $template(o, $($arg),*))
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
@ -25,3 +25,5 @@ impl<T: FnOnce(&mut Vec<u8>) -> std::io::Result<()> + Send> IntoResponse for Ren
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) use render;
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
@use super::base_html;
|
||||||
|
|
||||||
|
@()
|
||||||
|
|
||||||
|
@:base_html({
|
||||||
|
<form action="/admin/logout" method="post">
|
||||||
|
<button type="logout">log out</button>
|
||||||
|
</form>
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
@use super::base_html;
|
||||||
|
|
||||||
|
@()
|
||||||
|
|
||||||
|
@:base_html({
|
||||||
|
<form action="/admin/login" method="post">
|
||||||
|
<label for="password">admin password:</label>
|
||||||
|
<input type="text" id="password" name="password" required="true" />
|
||||||
|
<button type="submit">log in</button>
|
||||||
|
</form>
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
@use super::statics::*;
|
||||||
|
|
||||||
|
@(body: Content)
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/style.css" />
|
||||||
|
<title>pinussy</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
@:body()
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,9 @@
|
||||||
|
@use super::base_html;
|
||||||
|
@use poem::http::StatusCode;
|
||||||
|
|
||||||
|
@(status: &str, message: &str)
|
||||||
|
|
||||||
|
@:base_html({
|
||||||
|
<h1>error @status</h1>
|
||||||
|
<h2>@message</h2>
|
||||||
|
})
|
Loading…
Reference in New Issue