summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-09-04 12:13:54 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-09-04 12:13:54 -0400
commitcae21da31ff795cc21ec19288fcdc5fdb8a713c7 (patch)
treec3ae1f5fdfc6ebd703a9387b1108671c003b7eaa /src
parent2c999920d8f6f0b320960b01721e1f29f4078755 (diff)
Allow any login to create channels.
Diffstat (limited to 'src')
-rw-r--r--src/channel/mod.rs4
-rw-r--r--src/channel/repo.rs108
-rw-r--r--src/channel/routes.rs32
-rw-r--r--src/cli.rs6
-rw-r--r--src/id.rs7
-rw-r--r--src/index.rs93
-rw-r--r--src/lib.rs1
7 files changed, 232 insertions, 19 deletions
diff --git a/src/channel/mod.rs b/src/channel/mod.rs
new file mode 100644
index 0000000..238e116
--- /dev/null
+++ b/src/channel/mod.rs
@@ -0,0 +1,4 @@
+pub mod repo;
+mod routes;
+
+pub use self::routes::router;
diff --git a/src/channel/repo.rs b/src/channel/repo.rs
new file mode 100644
index 0000000..e6a5e5c
--- /dev/null
+++ b/src/channel/repo.rs
@@ -0,0 +1,108 @@
+use std::fmt;
+
+use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
+
+use crate::id::Id as BaseId;
+use crate::{error::BoxedError, login::repo::logins::Id as LoginId};
+
+pub trait Provider {
+ fn channels(&mut self) -> Channels;
+}
+
+impl<'c> Provider for Transaction<'c, Sqlite> {
+ fn channels(&mut self) -> Channels {
+ Channels(self)
+ }
+}
+
+pub struct Channels<'t>(&'t mut SqliteConnection);
+
+#[derive(Debug)]
+pub struct Channel {
+ pub id: Id,
+ pub name: String,
+}
+
+impl<'c> Channels<'c> {
+ /// Create a new channel.
+ pub async fn create(&mut self, name: &str) -> Result<Channel, BoxedError> {
+ let id = Id::generate();
+
+ let channel = sqlx::query_as!(
+ Channel,
+ r#"
+ insert
+ into channel (id, name)
+ values ($1, $2)
+ returning id as "id: Id", name
+ "#,
+ id,
+ name,
+ )
+ .fetch_one(&mut *self.0)
+ .await?;
+
+ Ok(channel)
+ }
+
+ /// Enrol a login in a channel.
+ pub async fn join(&mut self, channel: &Id, login: &LoginId) -> Result<(), BoxedError> {
+ sqlx::query!(
+ r#"
+ insert
+ into channel_member (channel, login)
+ values ($1, $2)
+ "#,
+ channel,
+ login,
+ )
+ .execute(&mut *self.0)
+ .await?;
+
+ Ok(())
+ }
+
+ pub async fn for_login(&mut self, login: &LoginId) -> Result<Vec<Channel>, BoxedError> {
+ let channels = sqlx::query_as!(
+ Channel,
+ r#"
+ select
+ channel.id as "id: Id",
+ channel.name
+ from channel
+ join channel_member
+ on (channel.id = channel_member.channel)
+ where channel_member.login = $1
+ order by channel.name
+ "#,
+ login,
+ )
+ .fetch_all(&mut *self.0)
+ .await?;
+
+ Ok(channels)
+ }
+}
+
+/// Stable identifier for a [Channel]. Prefixed with `C`.
+#[derive(Debug, sqlx::Type)]
+#[sqlx(transparent)]
+pub struct Id(BaseId);
+
+impl From<BaseId> for Id {
+ fn from(id: BaseId) -> Self {
+ Self(id)
+ }
+}
+
+impl Id {
+ pub fn generate() -> Self {
+ BaseId::generate("C")
+ }
+}
+
+impl fmt::Display for Id {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
diff --git a/src/channel/routes.rs b/src/channel/routes.rs
new file mode 100644
index 0000000..c8d6c3f
--- /dev/null
+++ b/src/channel/routes.rs
@@ -0,0 +1,32 @@
+use axum::{
+ extract::{Form, State},
+ response::{IntoResponse, Redirect},
+ routing::post,
+ Router,
+};
+use sqlx::sqlite::SqlitePool;
+
+use super::repo::Provider as _;
+use crate::{error::InternalError, login::repo::logins::Login};
+
+pub fn router() -> Router<SqlitePool> {
+ Router::new().route("/create", post(on_create))
+}
+
+#[derive(serde::Deserialize)]
+struct CreateRequest {
+ name: String,
+}
+
+async fn on_create(
+ State(db): State<SqlitePool>,
+ login: Login,
+ Form(form): Form<CreateRequest>,
+) -> Result<impl IntoResponse, InternalError> {
+ let mut tx = db.begin().await?;
+ let channel = tx.channels().create(&form.name).await?;
+ tx.channels().join(&channel.id, &login.id).await?;
+ tx.commit().await?;
+
+ Ok(Redirect::to("/"))
+}
diff --git a/src/cli.rs b/src/cli.rs
index 704c004..eef006e 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -6,7 +6,7 @@ use clap::Parser;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use tokio::net;
-use crate::{clock, error::BoxedError, index, login};
+use crate::{channel, clock, error::BoxedError, index, login};
pub type Result<T> = std::result::Result<T, BoxedError>;
@@ -63,7 +63,9 @@ impl Args {
}
fn routers() -> Router<SqlitePool> {
- index::router().merge(login::router())
+ [channel::router(), login::router()]
+ .into_iter()
+ .fold(index::router(), Router::merge)
}
fn started_msg(listener: &net::TcpListener) -> io::Result<String> {
diff --git a/src/id.rs b/src/id.rs
index f630107..6dbdc30 100644
--- a/src/id.rs
+++ b/src/id.rs
@@ -1,4 +1,5 @@
use rand::{seq::SliceRandom, thread_rng};
+use std::fmt;
// Make IDs that:
//
@@ -30,6 +31,12 @@ pub const ID_SIZE: usize = 15;
#[sqlx(transparent)]
pub struct Id(String);
+impl fmt::Display for Id {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
impl Id {
pub fn generate<T>(prefix: &str) -> T
where
diff --git a/src/index.rs b/src/index.rs
index a716af2..8ff9f7e 100644
--- a/src/index.rs
+++ b/src/index.rs
@@ -1,48 +1,79 @@
-use axum::{response::IntoResponse, routing::get, Router};
+use axum::{extract::State, routing::get, Router};
+use maud::Markup;
use sqlx::sqlite::SqlitePool;
-use crate::login::repo::logins::Login;
+use crate::{channel::repo::Provider as _, error::InternalError, login::repo::logins::Login};
-pub fn router() -> Router<SqlitePool> {
- Router::new().route("/", get(index))
+async fn index(
+ State(db): State<SqlitePool>,
+ login: Option<Login>,
+) -> Result<Markup, InternalError> {
+ match login {
+ None => Ok(templates::unauthenticated()),
+ Some(login) => index_authenticated(db, login).await,
+ }
+}
+
+async fn index_authenticated(db: SqlitePool, login: Login) -> Result<Markup, InternalError> {
+ let mut tx = db.begin().await?;
+ let channels = tx.channels().for_login(&login.id).await?;
+ tx.commit().await?;
+
+ Ok(templates::authenticated(login, &channels))
}
-async fn index(login: Option<Login>) -> impl IntoResponse {
- templates::index(login)
+pub fn router() -> Router<SqlitePool> {
+ Router::new().route("/", get(index))
}
mod templates {
use maud::{html, Markup, DOCTYPE};
- use crate::login::repo::logins::Login;
+ use crate::{channel::repo::Channel, login::repo::logins::Login};
- pub fn index(login: Option<Login>) -> Markup {
+ pub fn authenticated<'c>(
+ login: Login,
+ channels: impl IntoIterator<Item = &'c Channel>,
+ ) -> Markup {
html! {
(DOCTYPE)
head {
title { "hi" }
}
body {
- @match login {
- None => { (login_form()) }
- Some(login) => { (logout_form(&login.name)) }
+ section {
+ (channel_list(channels))
+ (create_channel())
+ }
+ section {
+ (logout_form(&login.name))
}
}
}
}
- fn login_form() -> Markup {
+ fn channel_list<'c>(channels: impl IntoIterator<Item = &'c Channel>) -> Markup {
html! {
- form action="/login" method="post" {
+ ul {
+ @for channel in channels {
+ li {
+ (channel.name) "(" (channel.id) ")"
+ }
+ }
+ }
+ }
+ }
+
+ fn create_channel() -> Markup {
+ html! {
+ form action="/create" method="post" {
label {
"name"
input name="name" type="text" {}
}
- label {
- "password"
- input name="password" type="password" {}
+ button {
+ "start channel"
}
- button { "hi" }
}
}
}
@@ -54,4 +85,32 @@ mod templates {
}
}
}
+
+ pub fn unauthenticated() -> Markup {
+ html! {
+ (DOCTYPE)
+ head {
+ title { "hi" }
+ }
+ body {
+ (login_form())
+ }
+ }
+ }
+
+ fn login_form() -> Markup {
+ html! {
+ form action="/login" method="post" {
+ label {
+ "login"
+ input name="name" type="text" {}
+ }
+ label {
+ "password"
+ input name="password" type="password" {}
+ }
+ button { "hi" }
+ }
+ }
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index 80cf5a4..3b8bbcd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
+mod channel;
pub mod cli;
mod clock;
mod error;