106 lines
2.7 KiB
Rust
106 lines
2.7 KiB
Rust
use axum::{
|
|
extract::State,
|
|
http::{header::SET_COOKIE, StatusCode},
|
|
response::{IntoResponse, Response},
|
|
routing::{get, post},
|
|
Json, Router,
|
|
};
|
|
use chrono::Duration;
|
|
use serde::Deserialize;
|
|
use serde_json::json;
|
|
use std::sync::Arc;
|
|
|
|
use crate::{
|
|
auth::{
|
|
hash::verify,
|
|
session::{Session, SessionRepository},
|
|
user::UserRepository,
|
|
},
|
|
database::Database,
|
|
http::{
|
|
error::ApiError,
|
|
json::JsonBody,
|
|
session::{RequireSession, RequireUser},
|
|
},
|
|
state::AppState,
|
|
};
|
|
|
|
#[derive(Deserialize)]
|
|
struct LoginRequest {
|
|
pub username: String,
|
|
pub password: String,
|
|
}
|
|
|
|
async fn login<Repo: UserRepository + SessionRepository>(
|
|
repository: Repo,
|
|
State(state): State<Arc<AppState>>,
|
|
JsonBody(payload): JsonBody<LoginRequest>,
|
|
) -> impl IntoResponse {
|
|
let user = repository
|
|
.find_user(payload.username.as_str())
|
|
.await
|
|
.map_err(ApiError::from)?;
|
|
|
|
let invalid = || -> ApiError {
|
|
ApiError::Client {
|
|
status: StatusCode::UNAUTHORIZED,
|
|
code: "invalid-login",
|
|
message: "No matching user was found",
|
|
}
|
|
};
|
|
|
|
let user = user.ok_or_else(invalid)?;
|
|
let plaintext = user.password.ok_or_else(invalid)?;
|
|
|
|
if !verify(&payload.password, &plaintext).map_err(ApiError::from)? {
|
|
return Err(invalid());
|
|
}
|
|
|
|
let session = repository
|
|
.create_session(user.id, Duration::seconds(state.config.session_duration))
|
|
.await?;
|
|
|
|
let token = session.token();
|
|
let mut response: Response =
|
|
Json(json!({ "session_token": token, "expires_at": session.expires_at.and_utc() }))
|
|
.into_response();
|
|
|
|
response.headers_mut().insert(
|
|
SET_COOKIE,
|
|
session
|
|
.cookie(&state.config.domain(), state.config.secure())
|
|
.parse()
|
|
.map_err(anyhow::Error::from)?,
|
|
);
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
async fn me(RequireUser(user): RequireUser) -> Result<String, ApiError<'static>> {
|
|
Ok(user.name)
|
|
}
|
|
|
|
async fn logout<Repo: SessionRepository>(
|
|
repository: Repo,
|
|
State(state): State<Arc<AppState>>,
|
|
RequireSession(session): RequireSession,
|
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
|
repository.destroy_session(session.id).await?;
|
|
|
|
let mut response: Response = Json(json!({ "ok": true })).into_response();
|
|
response.headers_mut().insert(
|
|
SET_COOKIE,
|
|
Session::cookie_for_delete(&state.config.domain(), state.config.secure())
|
|
.parse()
|
|
.map_err(anyhow::Error::from)?,
|
|
);
|
|
Ok(response)
|
|
}
|
|
|
|
pub fn router() -> Router<Arc<AppState>> {
|
|
Router::new()
|
|
.route("/login", post(login::<Database>))
|
|
.route("/logout", post(logout::<Database>))
|
|
.route("/me", get(me))
|
|
}
|