This commit is contained in:
parent
089163f79d
commit
9d6586063d
8 changed files with 40 additions and 47 deletions
|
@ -10,7 +10,7 @@ use crate::http::error::ApiError;
|
||||||
|
|
||||||
use super::{hash::random, user::User};
|
use super::{hash::random, user::User};
|
||||||
|
|
||||||
pub const USER_NOT_FOUND: ApiError<'static> = ApiError::ClientError {
|
pub const USER_NOT_FOUND: ApiError<'static> = ApiError::Client {
|
||||||
status: StatusCode::UNAUTHORIZED,
|
status: StatusCode::UNAUTHORIZED,
|
||||||
code: "user-not-found",
|
code: "user-not-found",
|
||||||
message: "The logged-in user was not found",
|
message: "The logged-in user was not found",
|
||||||
|
@ -49,7 +49,7 @@ impl Session {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
actor: user_id,
|
actor: user_id,
|
||||||
secret: secret,
|
secret,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
expires_at: now + duration,
|
expires_at: now + duration,
|
||||||
})
|
})
|
||||||
|
@ -109,7 +109,7 @@ impl Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(self: Self, pool: &Pool<Postgres>, duration: Duration) -> Result<Self> {
|
pub async fn refresh(self, pool: &Pool<Postgres>, duration: Duration) -> Result<Self> {
|
||||||
let expires_at = (Utc::now() + duration).naive_utc();
|
let expires_at = (Utc::now() + duration).naive_utc();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
|
@ -123,13 +123,13 @@ impl Session {
|
||||||
Ok(Session { expires_at, ..self })
|
Ok(Session { expires_at, ..self })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn token(self: &Self) -> String {
|
pub fn token(&self) -> String {
|
||||||
format!("{}:{}", self.id.as_u128(), self.secret)
|
format!("{}:{}", self.id.as_u128(), self.secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_token(token: &str) -> Result<(Uuid, String)> {
|
pub fn parse_token(token: &str) -> Result<(Uuid, String)> {
|
||||||
let (uuid_str, token_str) = token
|
let (uuid_str, token_str) = token
|
||||||
.split_once(":")
|
.split_once(':')
|
||||||
.ok_or_else(|| anyhow!("malformed token"))?;
|
.ok_or_else(|| anyhow!("malformed token"))?;
|
||||||
Ok((
|
Ok((
|
||||||
Uuid::from_u128(uuid_str.parse::<u128>()?),
|
Uuid::from_u128(uuid_str.parse::<u128>()?),
|
||||||
|
@ -137,7 +137,7 @@ impl Session {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn destroy(self: Self, pool: &Pool<Postgres>) -> Result<()> {
|
pub async fn destroy(&self, pool: &Pool<Postgres>) -> Result<()> {
|
||||||
sqlx::query!("DELETE FROM sessions WHERE id = $1", self.id)
|
sqlx::query!("DELETE FROM sessions WHERE id = $1", self.id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -152,7 +152,7 @@ impl Session {
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cookie(self: &Self, domain: &str, secure: bool) -> String {
|
pub fn cookie(&self, domain: &str, secure: bool) -> String {
|
||||||
Cookie::build("session", self.token())
|
Cookie::build("session", self.token())
|
||||||
.domain(domain)
|
.domain(domain)
|
||||||
.secure(secure)
|
.secure(secure)
|
||||||
|
|
|
@ -10,5 +10,5 @@ pub enum Error {
|
||||||
IdentifierNotAvailable,
|
IdentifierNotAvailable,
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
DatabaseError(#[from] sqlx::Error),
|
QueryFailed(#[from] sqlx::Error),
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ impl SiteRepository for Database {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(Error::NotFound.into());
|
return Err(Error::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -90,7 +90,7 @@ impl SiteRepository for Database {
|
||||||
};
|
};
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(Error::NotFound.into());
|
return Err(Error::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -10,12 +10,12 @@ use thiserror::Error;
|
||||||
use crate::content;
|
use crate::content;
|
||||||
|
|
||||||
// Generic errors
|
// Generic errors
|
||||||
const ERR_NOT_FOUND: ApiError<'static> = ApiError::ClientError {
|
const ERR_NOT_FOUND: ApiError<'static> = ApiError::Client {
|
||||||
status: StatusCode::NOT_FOUND,
|
status: StatusCode::NOT_FOUND,
|
||||||
code: "not-found",
|
code: "not-found",
|
||||||
message: "resource not found",
|
message: "resource not found",
|
||||||
};
|
};
|
||||||
const ERR_NOT_AVAILABLE: ApiError<'static> = ApiError::ClientError {
|
const ERR_NOT_AVAILABLE: ApiError<'static> = ApiError::Client {
|
||||||
status: StatusCode::CONFLICT,
|
status: StatusCode::CONFLICT,
|
||||||
code: "id-not-available",
|
code: "id-not-available",
|
||||||
message: "the chosen identifier is not available",
|
message: "the chosen identifier is not available",
|
||||||
|
@ -24,41 +24,41 @@ const ERR_NOT_AVAILABLE: ApiError<'static> = ApiError::ClientError {
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ApiError<'a> {
|
pub enum ApiError<'a> {
|
||||||
#[error("client error: <{code}> {message}")]
|
#[error("client error: <{code}> {message}")]
|
||||||
ClientError {
|
Client {
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
code: &'a str,
|
code: &'a str,
|
||||||
message: &'a str,
|
message: &'a str,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("unexpected internal error: {0}")]
|
#[error("unexpected internal error: {0}")]
|
||||||
InternalError(#[from] anyhow::Error),
|
Internal(#[from] anyhow::Error),
|
||||||
|
|
||||||
#[error("database returnede error: {0}")]
|
#[error("database returnede error: {0}")]
|
||||||
DatabaseError(#[from] sqlx::Error),
|
Database(#[from] sqlx::Error),
|
||||||
|
|
||||||
#[error("incoming JSON format error: {0}")]
|
#[error("incoming JSON format error: {0}")]
|
||||||
JSONFormatError(#[from] JsonRejection),
|
JSONFormat(#[from] JsonRejection),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for ApiError<'_> {
|
impl IntoResponse for ApiError<'_> {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
match self {
|
match self {
|
||||||
ApiError::InternalError(err) => (
|
ApiError::Internal(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({"code":"server-error", "message": err.to_string()})),
|
Json(json!({"code":"server-error", "message": err.to_string()})),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
ApiError::DatabaseError(err) => (
|
ApiError::Database(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({"code":"server-error", "message": err.to_string()})),
|
Json(json!({"code":"server-error", "message": err.to_string()})),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
ApiError::ClientError {
|
ApiError::Client {
|
||||||
status,
|
status,
|
||||||
code,
|
code,
|
||||||
message,
|
message,
|
||||||
} => (status, Json(json!({"code":code, "message": message}))).into_response(),
|
} => (status, Json(json!({"code":code, "message": message}))).into_response(),
|
||||||
ApiError::JSONFormatError(rejection) => {
|
ApiError::JSONFormat(rejection) => {
|
||||||
let status = match rejection {
|
let status = match rejection {
|
||||||
JsonRejection::JsonDataError(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
JsonRejection::JsonDataError(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
JsonRejection::JsonSyntaxError(_) => StatusCode::BAD_REQUEST,
|
JsonRejection::JsonSyntaxError(_) => StatusCode::BAD_REQUEST,
|
||||||
|
@ -80,7 +80,7 @@ impl From<content::Error> for ApiError<'_> {
|
||||||
match err {
|
match err {
|
||||||
content::Error::NotFound => ERR_NOT_FOUND,
|
content::Error::NotFound => ERR_NOT_FOUND,
|
||||||
content::Error::IdentifierNotAvailable => ERR_NOT_AVAILABLE,
|
content::Error::IdentifierNotAvailable => ERR_NOT_AVAILABLE,
|
||||||
content::Error::DatabaseError(err) => err.into(),
|
content::Error::QueryFailed(err) => err.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,16 +22,14 @@ use crate::{
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const INVALID_SESSION: ApiError = ApiError::ClientError {
|
pub const INVALID_SESSION: ApiError = ApiError::Client {
|
||||||
status: StatusCode::UNAUTHORIZED,
|
status: StatusCode::UNAUTHORIZED,
|
||||||
code: "authentication-required",
|
code: "authentication-required",
|
||||||
message: "Please log-in and submit a valid session as a cookie",
|
message: "Please log-in and submit a valid session as a cookie",
|
||||||
};
|
};
|
||||||
|
|
||||||
fn extract_session_token(header: &HeaderValue) -> Result<(Uuid, String)> {
|
fn extract_session_token(header: &HeaderValue) -> Result<(Uuid, String)> {
|
||||||
Ok(Session::parse_token(
|
Session::parse_token(Cookie::parse(header.to_str()?)?.value())
|
||||||
Cookie::parse(header.to_str()?)?.value(),
|
|
||||||
)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RequireUser(pub User);
|
pub struct RequireUser(pub User);
|
||||||
|
@ -78,18 +76,17 @@ pub async fn refresh_sessions<B>(
|
||||||
.get(COOKIE)
|
.get(COOKIE)
|
||||||
.and_then(|header| extract_session_token(header).ok())
|
.and_then(|header| extract_session_token(header).ok())
|
||||||
{
|
{
|
||||||
if let Some(Some((session, user))) = Session::find(&state.database, session_id).await.ok() {
|
if let Ok(Some((session, user))) = Session::find(&state.database, session_id).await {
|
||||||
// session validity requirements: secret must match, session must not have been expired
|
// session validity requirements: secret must match, session must not have been expired
|
||||||
if session.secret == session_secret && session.expires_at >= Utc::now().naive_utc() {
|
if session.secret == session_secret && session.expires_at >= Utc::now().naive_utc() {
|
||||||
// in the future we might wanna change the session secret, if we do, do it here!
|
// in the future we might wanna change the session secret, if we do, do it here!
|
||||||
if let Some((session, user)) = session
|
if let Ok((session, user)) = session
|
||||||
.refresh(
|
.refresh(
|
||||||
&state.database,
|
&state.database,
|
||||||
Duration::seconds(state.config.session_duration),
|
Duration::seconds(state.config.session_duration),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(|s| (s, user))
|
.map(|s| (s, user))
|
||||||
.ok()
|
|
||||||
{
|
{
|
||||||
let extensions = req.extensions_mut();
|
let extensions = req.extensions_mut();
|
||||||
extensions.insert(session.clone());
|
extensions.insert(session.clone());
|
||||||
|
|
|
@ -24,10 +24,10 @@ async fn bootstrap(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
.map_err(anyhow::Error::from)?;
|
.map_err(anyhow::Error::from)?;
|
||||||
|
|
||||||
if !empty {
|
if !empty {
|
||||||
return Err(ApiError::ClientError {
|
return Err(ApiError::Client {
|
||||||
status: StatusCode::BAD_REQUEST,
|
status: StatusCode::BAD_REQUEST,
|
||||||
code: "already-setup".into(),
|
code: "already-setup",
|
||||||
message: "The instance was already bootstrapped".into(),
|
message: "The instance was already bootstrapped",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ async fn bootstrap(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
|
|
||||||
User::create(
|
User::create(
|
||||||
&state.database,
|
&state.database,
|
||||||
&username,
|
username,
|
||||||
&password,
|
&password,
|
||||||
&[ROLE_SUPERADMIN].to_vec(),
|
&[ROLE_SUPERADMIN].to_vec(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,10 +35,10 @@ async fn login(
|
||||||
.map_err(ApiError::from)?;
|
.map_err(ApiError::from)?;
|
||||||
|
|
||||||
let invalid = || -> ApiError {
|
let invalid = || -> ApiError {
|
||||||
ApiError::ClientError {
|
ApiError::Client {
|
||||||
status: StatusCode::UNAUTHORIZED,
|
status: StatusCode::UNAUTHORIZED,
|
||||||
code: "invalid-login".into(),
|
code: "invalid-login",
|
||||||
message: "No matching user was found".into(),
|
message: "No matching user was found",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ async fn login(
|
||||||
let session = Session::create(
|
let session = Session::create(
|
||||||
&state.database,
|
&state.database,
|
||||||
user.id,
|
user.id,
|
||||||
Duration::seconds(state.config.session_duration.into()),
|
Duration::seconds(state.config.session_duration),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)?;
|
.map_err(ApiError::from)?;
|
||||||
|
@ -64,7 +64,7 @@ async fn login(
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
SET_COOKIE,
|
SET_COOKIE,
|
||||||
session
|
session
|
||||||
.cookie(state.config.domain().as_str(), state.config.secure())
|
.cookie(&state.config.domain(), state.config.secure())
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(anyhow::Error::from)?,
|
.map_err(anyhow::Error::from)?,
|
||||||
);
|
);
|
||||||
|
@ -85,7 +85,7 @@ async fn logout(
|
||||||
let mut response: Response = Json(json!({ "ok": true })).into_response();
|
let mut response: Response = Json(json!({ "ok": true })).into_response();
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
SET_COOKIE,
|
SET_COOKIE,
|
||||||
Session::cookie_for_delete(state.config.domain().as_str(), state.config.secure())
|
Session::cookie_for_delete(&state.config.domain(), state.config.secure())
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(anyhow::Error::from)?,
|
.map_err(anyhow::Error::from)?,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::Path,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
|
@ -7,10 +7,7 @@ use axum::{
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
content::page::PageRepository,
|
content::page::PageRepository, database::Database, http::error::ApiError, state::AppState,
|
||||||
database::Database,
|
|
||||||
http::{error::ApiError, session::RequireUser},
|
|
||||||
state::AppState,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn get_page<Repo: PageRepository>(
|
async fn get_page<Repo: PageRepository>(
|
||||||
|
@ -20,10 +17,9 @@ async fn get_page<Repo: PageRepository>(
|
||||||
Ok(Json(repository.get_page_from_url(&site, &slug).await?))
|
Ok(Json(repository.get_page_from_url(&site, &slug).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_page<Repo: PageRepository>(
|
async fn create_page<Repo: PageRepository>(//repository: Repo,
|
||||||
repository: Repo,
|
//Path(site): Path<String>,
|
||||||
Path(site): Path<String>,
|
//RequireUser(user): RequireUser,
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
Ok(Json("todo"))
|
Ok(Json("todo"))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue