use axum::{
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Redirect},
routing::{get, post},
Form, Router,
};
use serde::{Deserialize, Serialize};
use surrealdb::{engine::remote::ws::Client, Surreal};
use crate::{
error::AppError,
user::{AuthSession, Credentials, Password, SecretPassword, User},
util::ulid_to_suuid,
AppJson, AppState,
};
#[derive(Debug, Deserialize)]
struct NextUrl {
next: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
enum UserStatus {
Anonymous,
LoggedIn { username: String, code: u64 },
}
pub fn router() -> Router<AppState> {
Router::new()
.route("/login", post(login).get(status))
.route("/logout", get(logout))
.route("/signup", post(signup))
}
#[derive(Debug, Deserialize)]
struct Signup {
email: String,
username: String,
password: String,
}
#[derive(Serialize)]
struct SignupSafe {
email: String,
username: String,
password: SecretPassword,
}
impl From<Signup> for SignupSafe {
fn from(value: Signup) -> Self {
Self {
email: value.email.to_ascii_lowercase(),
username: value.username,
password: SecretPassword::new(Password::new(value.password)),
}
}
}
#[tracing::instrument(skip(db, auth_session))]
async fn signup(
mut auth_session: AuthSession,
State(db): State<Surreal<Client>>,
AppJson(signup): AppJson<Signup>,
) -> crate::error::Result<impl IntoResponse> {
let id = ulid::Ulid::new();
let user: Option<User> =
db.create(("user", ulid_to_suuid(id))).content::<SignupSafe>(signup.into()).await?;
let Some(user) = user else {
return Err(AppError::Arbitrary(
"could-not-create-user",
"could not create user in db".to_string(),
));
};
if let Err(e) = auth_session.login(&user).await {
tracing::error!("Failed to log in {}: {}", user.username, e);
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
}
Ok(StatusCode::NO_CONTENT.into_response())
}
#[tracing::instrument(skip(auth_session))]
async fn login(
mut auth_session: AuthSession,
Query(NextUrl { next }): Query<NextUrl>,
Form(creds): Form<Credentials>,
) -> impl IntoResponse {
let user = match auth_session.authenticate(creds.clone()).await {
Ok(Some(user)) => user,
Ok(None) => return AppError::InvalidCredentials.into_response(),
Err(e) => {
tracing::error!("{e}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
if let Err(e) = auth_session.login(&user).await {
tracing::error!("Failed to log in {}: {}", user.username, e);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
if let Some(ref next) = creds.next.or(next) {
Redirect::to(next).into_response()
} else {
StatusCode::NO_CONTENT.into_response()
}
}
async fn status(auth_session: AuthSession) -> AppJson<UserStatus> {
match auth_session.user {
Some(user) => AppJson(UserStatus::LoggedIn { code: user.code(), username: user.username }),
None => AppJson(UserStatus::Anonymous),
}
}
#[tracing::instrument(skip(auth_session))]
async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
match auth_session.logout().await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(e) => {
tracing::error!("Failed to log out user: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}