Allow to add emails
This commit is contained in:
parent
d016f2e95e
commit
026374fd7c
5 changed files with 126 additions and 8 deletions
4
.djlintrc
Normal file
4
.djlintrc
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"profile": "jinja",
|
||||||
|
"ignore": "D018"
|
||||||
|
}
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1277,6 +1277,7 @@ dependencies = [
|
||||||
"openidconnect",
|
"openidconnect",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_urlencoded",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tera",
|
"tera",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
82
src/main.rs
82
src/main.rs
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue