use crate::core::err_if_user_is_invalid::err_if_user_is_invalid;
use crate::persistence::lmdb::tables::users::User;
use crate::shared::{HttpError, HttpResult};
use crate::{core::AppState, SignupMode};
use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{extract::Host, headers::UserAgent, TypedHeader};
use base32::{encode, Alphabet};
use bytes::Bytes;
use pkarr::PublicKey;
use pubky_common::{capabilities::Capability, crypto::random_bytes, session::Session};
use std::collections::HashMap;
use tower_cookies::{
cookie::time::{Duration, OffsetDateTime},
cookie::SameSite,
Cookie, Cookies,
};
pub async fn signup(
State(state): State<AppState>,
user_agent: Option<TypedHeader<UserAgent>>,
cookies: Cookies,
Host(host): Host,
Query(params): Query<HashMap<String, String>>, body: Bytes,
) -> HttpResult<impl IntoResponse> {
let token = state.verifier.verify(&body)?;
let public_key = token.pubky();
let txn = state.db.env.read_txn()?;
let users = state.db.tables.users;
if users.get(&txn, public_key)?.is_some() {
return Err(HttpError::new_with_message(
StatusCode::CONFLICT,
"User already exists",
));
}
txn.commit()?;
if state.signup_mode == SignupMode::TokenRequired {
let signup_token_param = params
.get("signup_token")
.ok_or(HttpError::new_with_message(
StatusCode::BAD_REQUEST,
"signup token required",
))?;
if let Err(e) = state
.db
.validate_and_consume_signup_token(signup_token_param, public_key)
{
tracing::warn!("Failed to signup. Invalid signup token: {:?}", e);
return Err(HttpError::new_with_message(
StatusCode::UNAUTHORIZED,
"invalid signup token",
));
}
}
let mut wtxn = state.db.env.write_txn()?;
users.put(&mut wtxn, public_key, &User::default())?;
wtxn.commit()?;
create_session_and_cookie(
&state,
cookies,
&host,
public_key,
token.capabilities(),
user_agent,
)
}
pub async fn signin(
State(state): State<AppState>,
user_agent: Option<TypedHeader<UserAgent>>,
cookies: Cookies,
Host(host): Host,
body: Bytes,
) -> HttpResult<impl IntoResponse> {
let token = state.verifier.verify(&body)?;
let public_key = token.pubky();
let txn = state.db.env.read_txn()?;
let users = state.db.tables.users;
let user_exists = users.get(&txn, public_key)?.is_some();
txn.commit()?;
if !user_exists {
return Err(HttpError::new_with_message(
StatusCode::NOT_FOUND,
"User does not exist",
));
}
create_session_and_cookie(
&state,
cookies,
&host,
public_key,
token.capabilities(),
user_agent,
)
}
fn create_session_and_cookie(
state: &AppState,
cookies: Cookies,
host: &str,
public_key: &PublicKey,
capabilities: &[Capability],
user_agent: Option<TypedHeader<UserAgent>>,
) -> HttpResult<impl IntoResponse> {
err_if_user_is_invalid(public_key, &state.db, false)?;
let session_secret = encode(Alphabet::Crockford, &random_bytes::<16>());
let session = Session::new(
public_key,
capabilities,
user_agent.map(|ua| ua.to_string()),
)
.serialize();
let mut wtxn = state.db.env.write_txn()?;
state
.db
.tables
.sessions
.put(&mut wtxn, &session_secret, &session)?;
wtxn.commit()?;
let mut cookie = Cookie::new(public_key.to_string(), session_secret);
cookie.set_path("/");
if is_secure(host) {
cookie.set_secure(true);
cookie.set_same_site(SameSite::None);
}
cookie.set_http_only(true);
let one_year = Duration::days(365);
let expiry = OffsetDateTime::now_utc() + one_year;
cookie.set_max_age(one_year);
cookie.set_expires(expiry);
cookies.add(cookie);
Ok(session)
}
fn is_secure(host: &str) -> bool {
url::Host::parse(host)
.map(|host| match host {
url::Host::Domain(domain) => domain != "localhost",
_ => false,
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use pkarr::Keypair;
use super::*;
#[test]
fn test_is_secure() {
assert!(!is_secure(""));
assert!(!is_secure("127.0.0.1"));
assert!(!is_secure("167.86.102.121"));
assert!(!is_secure("[2001:0db8:0000:0000:0000:ff00:0042:8329]"));
assert!(!is_secure("localhost"));
assert!(!is_secure("localhost:23423"));
assert!(is_secure(&Keypair::random().public_key().to_string()));
assert!(is_secure("example.com"));
}
}