refactor: restructure using simple and advanced api
parent
4d37ddb780
commit
73928e78f4
|
@ -1,5 +1,5 @@
|
||||||
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||||
use diesel::{prelude::*, sqlite::Sqlite};
|
use diesel::prelude::*;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Request, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
|
||||||
middleware::Next,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
routing::post,
|
routing::post,
|
||||||
RequestExt, Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_extra::{
|
use axum_extra::{
|
||||||
extract::{
|
extract::{
|
||||||
|
@ -19,12 +16,11 @@ use crate::{
|
||||||
db::{Session, User},
|
db::{Session, User},
|
||||||
server::{
|
server::{
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
|
gpodder::SESSION_ID_COOKIE,
|
||||||
Context,
|
Context,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const SESSION_ID_COOKIE: &str = "sessionid";
|
|
||||||
|
|
||||||
pub fn router() -> Router<Context> {
|
pub fn router() -> Router<Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/{username}/login.json", post(post_login))
|
.route("/{username}/login.json", post(post_login))
|
||||||
|
@ -93,74 +89,3 @@ async fn post_logout(
|
||||||
Ok(jar)
|
Ok(jar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This middleware accepts
|
|
||||||
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
|
|
||||||
// SAFETY: this extractor's error type is Infallible
|
|
||||||
let jar: CookieJar = req.extract_parts().await.unwrap();
|
|
||||||
tracing::debug!("{:?}", jar);
|
|
||||||
let mut auth_user = None;
|
|
||||||
let mut new_session_id = None;
|
|
||||||
|
|
||||||
if let Some(session_id) = jar
|
|
||||||
.get(SESSION_ID_COOKIE)
|
|
||||||
.and_then(|c| c.value().parse::<i64>().ok())
|
|
||||||
{
|
|
||||||
match tokio::task::spawn_blocking(move || Session::user_from_id(&ctx.pool, session_id))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.map_err(AppError::Db)
|
|
||||||
{
|
|
||||||
Ok(user) => {
|
|
||||||
auth_user = user;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return err.into_response();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if let Ok(auth) = req
|
|
||||||
.extract_parts::<TypedHeader<Authorization<Basic>>>()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
match tokio::task::spawn_blocking(move || {
|
|
||||||
let user = User::by_username(&ctx.pool, auth.username())?.ok_or(AppError::NotFound)?;
|
|
||||||
|
|
||||||
if user.verify_password(auth.password()) {
|
|
||||||
Ok((Session::new_for_user(&ctx.pool, user.id)?, user))
|
|
||||||
} else {
|
|
||||||
Err(AppError::Unauthorized)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
Ok((session, user)) => {
|
|
||||||
auth_user = Some(user);
|
|
||||||
new_session_id = Some(session.id);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return err.into_response();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(user) = auth_user {
|
|
||||||
req.extensions_mut().insert(user);
|
|
||||||
let res = next.run(req).await;
|
|
||||||
|
|
||||||
if let Some(session_id) = new_session_id {
|
|
||||||
(
|
|
||||||
jar.add(
|
|
||||||
Cookie::build((SESSION_ID_COOKIE, session_id.to_string()))
|
|
||||||
.expires(Expiration::Session),
|
|
||||||
),
|
|
||||||
res,
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
} else {
|
|
||||||
res
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
StatusCode::UNAUTHORIZED.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,18 +4,19 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Extension, Json, Router,
|
Extension, Json, Router,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{self, User},
|
db::{self, User},
|
||||||
server::{
|
server::{
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
|
gpodder::{
|
||||||
|
auth_middleware,
|
||||||
|
models::{Device, DevicePatch, DeviceType},
|
||||||
|
},
|
||||||
Context,
|
Context,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::auth::auth_middleware;
|
|
||||||
|
|
||||||
pub fn router(ctx: Context) -> Router<Context> {
|
pub fn router(ctx: Context) -> Router<Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/{username}", get(get_devices))
|
.route("/{username}", get(get_devices))
|
||||||
|
@ -23,48 +24,6 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
|
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum DeviceType {
|
|
||||||
Desktop,
|
|
||||||
Laptop,
|
|
||||||
Mobile,
|
|
||||||
Server,
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DeviceType> for db::DeviceType {
|
|
||||||
fn from(value: DeviceType) -> Self {
|
|
||||||
match value {
|
|
||||||
DeviceType::Desktop => Self::Desktop,
|
|
||||||
DeviceType::Laptop => Self::Laptop,
|
|
||||||
DeviceType::Mobile => Self::Mobile,
|
|
||||||
DeviceType::Server => Self::Server,
|
|
||||||
DeviceType::Other => Self::Other,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<db::DeviceType> for DeviceType {
|
|
||||||
fn from(value: db::DeviceType) -> Self {
|
|
||||||
match value {
|
|
||||||
db::DeviceType::Desktop => Self::Desktop,
|
|
||||||
db::DeviceType::Laptop => Self::Laptop,
|
|
||||||
db::DeviceType::Mobile => Self::Mobile,
|
|
||||||
db::DeviceType::Server => Self::Server,
|
|
||||||
db::DeviceType::Other => Self::Other,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Device {
|
|
||||||
id: String,
|
|
||||||
caption: String,
|
|
||||||
r#type: DeviceType,
|
|
||||||
subscriptions: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_devices(
|
async fn get_devices(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
Path(username): Path<String>,
|
Path(username): Path<String>,
|
||||||
|
@ -94,12 +53,6 @@ async fn get_devices(
|
||||||
Ok(Json(devices))
|
Ok(Json(devices))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct DevicePatch {
|
|
||||||
caption: Option<String>,
|
|
||||||
r#type: Option<DeviceType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn post_device(
|
async fn post_device(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
Path((_username, id)): Path<(String, String)>,
|
Path((_username, id)): Path<(String, String)>,
|
|
@ -0,0 +1,12 @@
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::Context;
|
||||||
|
|
||||||
|
mod auth;
|
||||||
|
mod devices;
|
||||||
|
|
||||||
|
pub fn router(ctx: Context) -> Router<Context> {
|
||||||
|
Router::new()
|
||||||
|
.nest("/auth", auth::router())
|
||||||
|
.nest("/devices", devices::router(ctx.clone()))
|
||||||
|
}
|
|
@ -1,18 +1,34 @@
|
||||||
mod auth;
|
mod advanced;
|
||||||
mod devices;
|
mod models;
|
||||||
|
mod simple;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
http::{HeaderName, HeaderValue},
|
extract::{Request, State},
|
||||||
Router,
|
http::{HeaderName, HeaderValue, StatusCode},
|
||||||
|
middleware::Next,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
RequestExt, Router,
|
||||||
|
};
|
||||||
|
use axum_extra::{
|
||||||
|
extract::{
|
||||||
|
cookie::{Cookie, Expiration},
|
||||||
|
CookieJar,
|
||||||
|
},
|
||||||
|
headers::{authorization::Basic, Authorization},
|
||||||
|
TypedHeader,
|
||||||
};
|
};
|
||||||
use tower_http::set_header::SetResponseHeaderLayer;
|
use tower_http::set_header::SetResponseHeaderLayer;
|
||||||
|
|
||||||
|
use crate::{db, server::error::AppError};
|
||||||
|
|
||||||
use super::Context;
|
use super::Context;
|
||||||
|
|
||||||
|
const SESSION_ID_COOKIE: &str = "sessionid";
|
||||||
|
|
||||||
pub fn router(ctx: Context) -> Router<Context> {
|
pub fn router(ctx: Context) -> Router<Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/auth", auth::router())
|
.merge(simple::router(ctx.clone()))
|
||||||
.nest("/devices", devices::router(ctx))
|
.nest("/api/2", advanced::router(ctx))
|
||||||
// https://gpoddernet.readthedocs.io/en/latest/api/reference/general.html#cors
|
// https://gpoddernet.readthedocs.io/en/latest/api/reference/general.html#cors
|
||||||
// All endpoints should send this CORS header value so the endpoints can be used from web
|
// All endpoints should send this CORS header value so the endpoints can be used from web
|
||||||
// applications
|
// applications
|
||||||
|
@ -21,3 +37,75 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
HeaderValue::from_static("*"),
|
HeaderValue::from_static("*"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This middleware accepts
|
||||||
|
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
|
||||||
|
// SAFETY: this extractor's error type is Infallible
|
||||||
|
let jar: CookieJar = req.extract_parts().await.unwrap();
|
||||||
|
tracing::debug!("{:?}", jar);
|
||||||
|
let mut auth_user = None;
|
||||||
|
let mut new_session_id = None;
|
||||||
|
|
||||||
|
if let Some(session_id) = jar
|
||||||
|
.get(SESSION_ID_COOKIE)
|
||||||
|
.and_then(|c| c.value().parse::<i64>().ok())
|
||||||
|
{
|
||||||
|
match tokio::task::spawn_blocking(move || db::Session::user_from_id(&ctx.pool, session_id))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.map_err(AppError::Db)
|
||||||
|
{
|
||||||
|
Ok(user) => {
|
||||||
|
auth_user = user;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return err.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if let Ok(auth) = req
|
||||||
|
.extract_parts::<TypedHeader<Authorization<Basic>>>()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
match tokio::task::spawn_blocking(move || {
|
||||||
|
let user =
|
||||||
|
db::User::by_username(&ctx.pool, auth.username())?.ok_or(AppError::NotFound)?;
|
||||||
|
|
||||||
|
if user.verify_password(auth.password()) {
|
||||||
|
Ok((db::Session::new_for_user(&ctx.pool, user.id)?, user))
|
||||||
|
} else {
|
||||||
|
Err(AppError::Unauthorized)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
Ok((session, user)) => {
|
||||||
|
auth_user = Some(user);
|
||||||
|
new_session_id = Some(session.id);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return err.into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(user) = auth_user {
|
||||||
|
req.extensions_mut().insert(user);
|
||||||
|
let res = next.run(req).await;
|
||||||
|
|
||||||
|
if let Some(session_id) = new_session_id {
|
||||||
|
(
|
||||||
|
jar.add(
|
||||||
|
Cookie::build((SESSION_ID_COOKIE, session_id.to_string()))
|
||||||
|
.expires(Expiration::Session),
|
||||||
|
),
|
||||||
|
res,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StatusCode::UNAUTHORIZED.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::db;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DeviceType {
|
||||||
|
Desktop,
|
||||||
|
Laptop,
|
||||||
|
Mobile,
|
||||||
|
Server,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DeviceType> for db::DeviceType {
|
||||||
|
fn from(value: DeviceType) -> Self {
|
||||||
|
match value {
|
||||||
|
DeviceType::Desktop => Self::Desktop,
|
||||||
|
DeviceType::Laptop => Self::Laptop,
|
||||||
|
DeviceType::Mobile => Self::Mobile,
|
||||||
|
DeviceType::Server => Self::Server,
|
||||||
|
DeviceType::Other => Self::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<db::DeviceType> for DeviceType {
|
||||||
|
fn from(value: db::DeviceType) -> Self {
|
||||||
|
match value {
|
||||||
|
db::DeviceType::Desktop => Self::Desktop,
|
||||||
|
db::DeviceType::Laptop => Self::Laptop,
|
||||||
|
db::DeviceType::Mobile => Self::Mobile,
|
||||||
|
db::DeviceType::Server => Self::Server,
|
||||||
|
db::DeviceType::Other => Self::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Device {
|
||||||
|
pub id: String,
|
||||||
|
pub caption: String,
|
||||||
|
pub r#type: DeviceType,
|
||||||
|
pub subscriptions: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct DevicePatch {
|
||||||
|
pub caption: Option<String>,
|
||||||
|
pub r#type: Option<DeviceType>,
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::Context;
|
||||||
|
|
||||||
|
pub fn router(ctx: Context) -> Router<Context> {
|
||||||
|
Router::new()
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ pub struct Context {
|
||||||
|
|
||||||
pub fn app(ctx: Context) -> Router {
|
pub fn app(ctx: Context) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/api/2", gpodder::router(ctx.clone()))
|
.merge(gpodder::router(ctx.clone()))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(ctx)
|
.with_state(ctx)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue