Allow to add emails

This commit is contained in:
traxys 2023-08-28 23:33:57 +02:00
parent d016f2e95e
commit 026374fd7c
5 changed files with 126 additions and 8 deletions

4
.djlintrc Normal file
View file

@ -0,0 +1,4 @@
{
"profile": "jinja",
"ignore": "D018"
}

1
Cargo.lock generated
View file

@ -1277,6 +1277,7 @@ dependencies = [
"openidconnect", "openidconnect",
"parking_lot", "parking_lot",
"serde", "serde",
"serde_urlencoded",
"sqlx", "sqlx",
"tera", "tera",
"thiserror", "thiserror",

View file

@ -16,6 +16,7 @@ once_cell = "1.18.0"
openidconnect = "3.3.0" openidconnect = "3.3.0"
parking_lot = "0.12.1" parking_lot = "0.12.1"
serde = { version = "1.0.183", features = ["derive"] } serde = { version = "1.0.183", features = ["derive"] }
serde_urlencoded = "0.7.1"
sqlx = { version = "0.7.1", features = ["runtime-tokio", "postgres", "uuid", "migrate"] } sqlx = { version = "0.7.1", features = ["runtime-tokio", "postgres", "uuid", "migrate"] }
tera = "1.19.0" tera = "1.19.0"
thiserror = "1.0.44" thiserror = "1.0.44"

View file

@ -1,5 +1,6 @@
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
fmt::Display,
net::SocketAddr, net::SocketAddr,
sync::Arc, sync::Arc,
}; };
@ -119,6 +120,8 @@ struct Settings {
address: String, address: String,
database_url: String, database_url: String,
mail_domain: String,
domain: String, domain: String,
oidc_endpoint: String, oidc_endpoint: String,
@ -314,6 +317,7 @@ impl OpenidConnector {
struct AppState { struct AppState {
jwt_secret: HS256Key, jwt_secret: HS256Key,
db: PgPool, db: PgPool,
mail_domain: String,
oidc: OpenidConnector, oidc: OpenidConnector,
} }
@ -486,33 +490,42 @@ where
} }
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
struct Mail { struct Mail {
mail: String, mail: String,
} }
async fn home(state: State<Arc<AppState>>, User(user): User) -> Result<Html<String>, Error> { #[derive(Serialize, Deserialize)]
struct HomeQuery {
user_error: Option<UserError>,
}
async fn home(
state: State<Arc<AppState>>,
User(user): User,
Query(query): Query<HomeQuery>,
) -> Result<Html<String>, Error> {
let mails = sqlx::query_as!(Mail, "SELECT mail FROM emails WHERE id = $1", user) let mails = sqlx::query_as!(Mail, "SELECT mail FROM emails WHERE id = $1", user)
.fetch_all(&state.db) .fetch_all(&state.db)
.await?; .await?;
let mut context = tera::Context::new(); let mut context = tera::Context::new();
context.insert("mails", &mails); context.insert("mails", &mails);
context.insert("mail_domain", &state.mail_domain);
if let Some(err) = query.user_error {
tracing::info!("User error: {err:?}");
context.insert("user_error", &err.to_string());
}
context.extend(global_context()); context.extend(global_context());
Ok(Html(TEMPLATES.render("home.html", &context)?)) Ok(Html(TEMPLATES.render("home.html", &context)?))
} }
#[derive(Deserialize, Debug)]
struct MailDelete {
mail: String,
}
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
async fn delete_mail( async fn delete_mail(
state: State<Arc<AppState>>, state: State<Arc<AppState>>,
User(user): User, User(user): User,
Form(delete): Form<MailDelete>, Form(delete): Form<Mail>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
let rows_affected = sqlx::query!( let rows_affected = sqlx::query!(
"DELETE FROM emails WHERE id = $1 AND mail = $2", "DELETE FROM emails WHERE id = $1 AND mail = $2",
@ -531,6 +544,57 @@ async fn delete_mail(
Ok(Redirect::to("/")) Ok(Redirect::to("/"))
} }
#[derive(Serialize, Deserialize, Debug)]
enum UserError {
MailAlreadyExists,
}
impl Display for UserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserError::MailAlreadyExists => {
write!(f, "email address is already used by another user")
}
}
}
}
async fn add_mail(
state: State<Arc<AppState>>,
User(user): User,
Form(add): Form<Mail>,
) -> Result<Redirect, Error> {
let has_mail = sqlx::query!(
"SELECT COUNT(*) FROM emails WHERE id != $1 AND mail = $2",
user,
add.mail
)
.fetch_one(&state.db)
.await?
.count
.expect("count should not be null");
if has_mail != 0 {
Ok(Redirect::to(&format!(
"/?{}",
serde_urlencoded::to_string(&HomeQuery {
user_error: Some(UserError::MailAlreadyExists)
})
.expect("could not generate query")
)))
} else {
sqlx::query!(
"INSERT INTO emails (id, mail) VALUES ($1, $2) ON CONFLICT DO NOTHING",
user,
add.mail
)
.execute(&state.db)
.await?;
Ok(Redirect::to("/"))
}
}
#[tokio::main] #[tokio::main]
async fn main() -> color_eyre::Result<()> { async fn main() -> color_eyre::Result<()> {
color_eyre::install()?; color_eyre::install()?;
@ -562,11 +626,13 @@ async fn main() -> color_eyre::Result<()> {
.route("/login/redirect/:id", get(redirected)) .route("/login/redirect/:id", get(redirected))
.route("/", get(home)) .route("/", get(home))
.route("/mail/delete", post(delete_mail)) .route("/mail/delete", post(delete_mail))
.route("/mail/add", post(add_mail))
.fallback(page_not_found) .fallback(page_not_found)
.with_state(Arc::new(AppState { .with_state(Arc::new(AppState {
db, db,
oidc, oidc,
jwt_secret: config.jwt_secret.0, jwt_secret: config.jwt_secret.0,
mail_domain: config.mail_domain,
})); }));
Ok(axum::Server::bind(&addr) Ok(axum::Server::bind(&addr)

View file

@ -8,6 +8,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1 class="title is-1">Mail management</h1> <h1 class="title is-1">Mail management</h1>
{% if user_error %}<div class="alert alert-danger">{{ user_error }}</div>{% endif %}
<h2 class="title is-2">Mails</h2> <h2 class="title is-2">Mails</h2>
<ul class="list-group"> <ul class="list-group">
{% for mail in mails %} {% for mail in mails %}
@ -43,5 +44,50 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<button type="button"
class="btn btn-primary mt-2"
data-bs-toggle="modal"
data-bs-target="#mailAdd">Add new mail</button>
<div class="modal fade"
tabindex="-1"
id="mailAdd"
aria-labelledby="mailAddLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="mailAddLabel">Add new mail</h1>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div> </div>
<div class="modal-body">
<form action="/mail/add" method="post" id="mailAddForm">
<div class="form-floating mb-3">
<input type="email"
class="form-control"
id="floatingAddMail"
placeholder="mail@{{ mail_domain }}"
name="mail"
value="@{{ mail_domain }}">
<label for="floatingAddMail">Email address</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" form="mailAddForm">Add</button>
</div>
</div>
</div>
</div>
</div>
<script>
const addModal = document.getElementById('mailAdd')
const addInput = document.getElementById('floatingAddMail')
addModal.addEventListener('shown.bs.modal', () => {
addInput.focus()
})
</script>
{% endblock content %} {% endblock content %}