Skip to content

Commit

Permalink
db, mail, user login
Browse files Browse the repository at this point in the history
  • Loading branch information
foltik committed Dec 15, 2024
1 parent e7166b9 commit be6c413
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 77 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/target
Cargo.lock
*.sqlite
*.sqlite-*
23 changes: 10 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,23 @@ edition = "2021"


[dependencies]
axum = { version = "0.7", default-features = false, features = ["json", "ws"] }
axum = { version = "0.7", default-features = false, features = ["query", "form", "matched-path"] }
axum-server = { version = "0.7", features = ["tls-rustls"] }
tower-http = { version = "0.6", features = ["fs"] }
axum-extra = { version = "0.9", features = ["cookie"] }
tower-http = { version = "0.6", features = ["fs", "trace"] }
tera = "1"

sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio"] }
lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "pool", "smtp-transport", "tokio1", "tokio1-rustls-tls", "serde"] }
tokio = { version = "1", features = ["rt-multi-thread", "fs", "net", "sync", "macros"] }
webrtc-util = "0.9"
webrtc-srtp = "0.13"
rtp = "0.11"
opus = "0.3"
# cpal = "0.15"
rustls = "0.23"

anyhow = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
serde = { version = "1", features = ["derive"] }
toml = "0.8"
bytes = "1"
bytemuck = "1"
ringbuf = "0.4"
quanta = "0.12"

rand = "0.8"
mimalloc = "*"

[profile.dev]
opt-level = 1
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ git clone https://github.com/foltik/wlsd
cd wlsd
```

Build and run:
```sh
cargo run config/dev.toml
```

To automatically recompile and rerun when you make changes, use `cargo-watch`:
```sh
cargo install cargo-watch
cargo watch -x 'run config/dev.toml'
```

Use [mailtutan](https://github.com/mailtutan/mailtutan) for local testing of email functionality:
```sh
cargo install mailtutan
mailtutan
```

## Workflow

* Make commits in a separate branch, and open a PR against `main`
Expand Down
6 changes: 5 additions & 1 deletion config/dev.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
[app]
url = "https://localhost:4433"
db = "db.sqlite"

[http]
addr = "0.0.0.0:8080"

[https]
addr = "0.0.0.0:4433"
cert = "config/selfsigned.cert"
key = "config/selfsigned.key"

[mail]
addr = "smtp://localhost:1025"
from = "WLSD <[email protected]>"
6 changes: 5 additions & 1 deletion config/prod.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
[app]
url = "https://wlsd.foltz.io"
db = "db.sqlite"

[http]
addr = "0.0.0.0:80"

[https]
addr = "0.0.0.0:443"
cert = "config/selfsigned.cert"
key = "config/selfsigned.key"

[mail]
addr = "smtp://localhost:1080"
from = "WLSD <[email protected]>"
168 changes: 168 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use std::{sync::Arc, time::Duration};

use crate::*;
use anyhow::Result;
use axum::{
extract::{MatchedPath, Query, Request, State},
http::{header, StatusCode},
response::{Html, IntoResponse, Response},
routing::{get, post},
Form, Router,
};
use axum_extra::extract::CookieJar;
use lettre::message::Mailbox;
use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing::Span;

#[derive(Clone)]
#[allow(unused)]
struct AppState {
config: Config,
templates: Tera,
db: Db,
mail: Mail,
}

pub async fn build(config: Config) -> Result<Router> {
let state = app::AppState {
config: config.clone(),
templates: Tera::new("templates/*")?,
db: Db::connect(&config.app.db).await?,
mail: Mail::connect(config.mail).await?,
};

let router = Router::new()
.route("/", get(home))
.route("/login", post(login_form))
.route("/login", get(login))
.route("/register", get(register))
.route("/register", post(register_form))
.nest_service("/assets", ServeDir::new("assets"))
.layer(
TraceLayer::new_for_http()
.make_span_with(|req: &Request<_>| {
let path = match req.extensions().get::<MatchedPath>() {
Some(path) => path.as_str(),
None => req.uri().path(),
};
tracing::info_span!("request", method = ?req.method(), path, status = tracing::field::Empty)
})
.on_request(|_req: &Request<_>, _span: &Span| {})
.on_response(|res: &Response, latency: Duration, span: &Span| {
span.record("status", res.status().as_u16());
tracing::info!("handled in {latency:?}");
}),
)
.with_state(Arc::new(state));
Ok(router)
}

async fn home(State(state): State<Arc<AppState>>, cookies: CookieJar) -> AppResult<Response> {
let mut ctx = tera::Context::new();
ctx.insert("message", "Hello, world!");

if let Some(session_token) = cookies.get("session") {
let Some(user) = state.db.lookup_user_from_session_token(session_token.value()).await? else {
return Ok(StatusCode::FORBIDDEN.into_response());
};
ctx.insert("user", &user);
}

let html = state.templates.render("home.tera.html", &ctx).unwrap();
Ok(Html(html).into_response())
}

#[derive(serde::Deserialize)]
struct LoginForm {
email: Mailbox,
}
async fn login_form(
State(state): State<Arc<AppState>>,
Form(form): Form<LoginForm>,
) -> AppResult<impl IntoResponse> {
let login_token = state.db.create_login_token(&form.email).await?;

let url = &state.config.app.url;
let url = match state.db.lookup_user_by_email(&form.email).await? {
Some(_) => format!("{url}/login?token={login_token}"),
None => format!("{url}/register?token={login_token}"),
};

let msg = state.mail.builder().to(form.email).body(url)?;
state.mail.send(msg).await?;

Ok("Check your email!")
}

#[derive(serde::Deserialize)]
struct LoginQuery {
token: String,
}
async fn login(State(state): State<Arc<AppState>>, Query(login): Query<LoginQuery>) -> AppResult<Response> {
let Some(user) = state.db.lookup_user_by_login_token(&login.token).await? else {
return Ok(StatusCode::FORBIDDEN.into_response());
};

let session_token = state.db.create_session_token(user.id).await?;
let headers = (
// TODO: expiration date
[(header::SET_COOKIE, format!("session={session_token}; Secure; Secure"))],
Redirect::to(&state.config.app.url),
);
Ok(headers.into_response())
}

#[derive(serde::Deserialize)]
struct RegisterQuery {
token: String,
}
async fn register(
State(state): State<Arc<AppState>>,
Query(register): Query<RegisterQuery>,
) -> AppResult<Response> {
let mut ctx = tera::Context::new();
ctx.insert("token", &register.token);
let html = state.templates.render("register.tera.html", &ctx).unwrap();
Ok(Html(html).into_response())
}

#[derive(serde::Deserialize)]
struct RegisterForm {
token: String,
first_name: String,
last_name: String,
}
async fn register_form(
State(state): State<Arc<AppState>>,
Form(form): Form<RegisterForm>,
) -> AppResult<Response> {
let Some(email) = state.db.lookup_email_by_login_token(&form.token).await? else {
return Ok(StatusCode::FORBIDDEN.into_response());
};

let user_id = state.db.create_user(&form.first_name, &form.last_name, &email).await?;
let session_token = state.db.create_session_token(user_id).await?;

// TODO: expiration date on the cookie
let headers = (
[(header::SET_COOKIE, format!("session={session_token}; Secure; Secure"))],
Redirect::to(&state.config.app.url),
);
Ok(headers.into_response())
}

struct AppError(anyhow::Error);
type AppResult<T> = Result<T, AppError>;
impl IntoResponse for AppError {
fn into_response(self) -> Response {
// TODO: add a `dev` mode to `config.app`, and:
// * when enabled, respond with a stack trace
// * when disabled, respond with a generic error message that doesn't leak any details
(StatusCode::INTERNAL_SERVER_ERROR, format!("Error: {}", self.0)).into_response()
}
}
impl<E: Into<anyhow::Error>> From<E> for AppError {
fn from(e: E) -> Self {
Self(e.into())
}
}
11 changes: 10 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use lettre::message::Mailbox;
use std::{net::SocketAddr, path::PathBuf};

pub async fn load(file: &str) -> Result<Config> {
Expand All @@ -10,12 +11,14 @@ pub async fn load(file: &str) -> Result<Config> {
pub struct Config {
pub app: ServerConfig,
pub http: HttpConfig,
pub https: Option<HttpsConfig>,
pub https: HttpsConfig,
pub mail: MailConfig,
}

#[derive(Clone, Debug, serde::Deserialize)]
pub struct ServerConfig {
pub url: String,
pub db: PathBuf,
}

#[derive(Clone, Debug, serde::Deserialize)]
Expand All @@ -29,3 +32,9 @@ pub struct HttpsConfig {
pub cert: PathBuf,
pub key: PathBuf,
}

#[derive(Clone, Debug, serde::Deserialize)]
pub struct MailConfig {
pub addr: String,
pub from: Mailbox,
}
Loading

0 comments on commit be6c413

Please sign in to comment.