Extract some functions to the app module: preparation for integration tests.
This commit is contained in:
parent
898f2e1160
commit
7f94159852
14 changed files with 605 additions and 521 deletions
437
backend/src/app.rs
Normal file
437
backend/src/app.rs
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use axum::{
|
||||
BoxError, Router, ServiceExt,
|
||||
error_handling::HandleErrorLayer,
|
||||
extract::{
|
||||
ConnectInfo, Extension, FromRef, Request, State,
|
||||
connect_info::IntoMakeServiceWithConnectInfo,
|
||||
},
|
||||
http::{StatusCode, Uri},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::{delete, get, patch, post, put},
|
||||
};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use chrono::prelude::*;
|
||||
use itertools::Itertools;
|
||||
use tower::layer::Layer;
|
||||
use tower::{ServiceBuilder, buffer::BufferLayer, limit::RateLimitLayer};
|
||||
use tower_http::{
|
||||
services::{ServeDir, ServeFile},
|
||||
trace::TraceLayer,
|
||||
};
|
||||
use tracing::{Level, event};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
consts,
|
||||
data::{db, model},
|
||||
log::Log,
|
||||
ron_utils, services,
|
||||
translation::{self, Tr},
|
||||
utils,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub config: Config,
|
||||
pub db_connection: db::Connection,
|
||||
pub log: Log,
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for Config {
|
||||
fn from_ref(app_state: &AppState) -> Config {
|
||||
app_state.config.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for db::Connection {
|
||||
fn from_ref(app_state: &AppState) -> db::Connection {
|
||||
app_state.db_connection.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for Log {
|
||||
fn from_ref(app_state: &AppState) -> Log {
|
||||
app_state.log.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::response::IntoResponse for db::DBError {
|
||||
fn into_response(self) -> Response {
|
||||
ron_utils::ron_error(StatusCode::INTERNAL_SERVER_ERROR, &self.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] db::DBError),
|
||||
|
||||
#[error("Template error: {0}")]
|
||||
Render(#[from] askama::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AppError>;
|
||||
|
||||
impl axum::response::IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub user: Option<model::User>,
|
||||
pub tr: Tr,
|
||||
pub dark_theme: bool,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn first_day_of_the_week(&self) -> Weekday {
|
||||
if let Some(user) = &self.user {
|
||||
user.first_day_of_the_week
|
||||
} else {
|
||||
self.tr.first_day_of_week()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Lang(Option<String>);
|
||||
|
||||
// TODO: Refactor the signature into something like 'impl Service<...>'.
|
||||
pub fn make_service(
|
||||
state: AppState,
|
||||
) -> IntoMakeServiceWithConnectInfo<
|
||||
tower::util::MapRequest<Router, fn(Request<axum::body::Body>) -> Request<axum::body::Body>>,
|
||||
SocketAddr,
|
||||
>
|
||||
// ) -> impl Service<axum::serve::IncomingStream<'static, L>, Error = core::convert::Infallible, Response = S>
|
||||
// where
|
||||
// L: axum::serve::Listener,
|
||||
// S: Service<Request, Response = Response, Error = core::convert::Infallible>
|
||||
// + Clone
|
||||
// + Send
|
||||
// + 'static,
|
||||
// S::Future: Send,
|
||||
// T: ,
|
||||
// std::net::SocketAddr:
|
||||
// axum::extract::connect_info::Connected<axum::serve::IncomingStream<'static, L>>,
|
||||
{
|
||||
let ron_api_routes = Router::new()
|
||||
// Disabled: update user profile is now made with a post data ('edit_user_post').
|
||||
// .route("/user/update", put(services::ron::update_user))
|
||||
.route("/lang", put(services::ron::set_lang))
|
||||
.route("/recipe/titles", get(services::ron::recipe::get_titles))
|
||||
.route("/recipe/title", patch(services::ron::recipe::set_title))
|
||||
.route(
|
||||
"/recipe/description",
|
||||
patch(services::ron::recipe::set_description),
|
||||
)
|
||||
.route(
|
||||
"/recipe/servings",
|
||||
patch(services::ron::recipe::set_servings),
|
||||
)
|
||||
.route(
|
||||
"/recipe/estimated_time",
|
||||
patch(services::ron::recipe::set_estimated_time),
|
||||
)
|
||||
.route(
|
||||
"/recipe/tags",
|
||||
get(services::ron::recipe::get_tags)
|
||||
.post(services::ron::recipe::add_tags)
|
||||
.delete(services::ron::recipe::rm_tags),
|
||||
)
|
||||
.route(
|
||||
"/recipe/difficulty",
|
||||
patch(services::ron::recipe::set_difficulty),
|
||||
)
|
||||
.route(
|
||||
"/recipe/language",
|
||||
patch(services::ron::recipe::set_language),
|
||||
)
|
||||
.route(
|
||||
"/recipe/is_public",
|
||||
patch(services::ron::recipe::set_is_public),
|
||||
)
|
||||
.route("/recipe", delete(services::ron::recipe::rm))
|
||||
.route("/recipe/groups", get(services::ron::recipe::get_groups))
|
||||
.route(
|
||||
"/recipe/group",
|
||||
post(services::ron::recipe::add_group).delete(services::ron::recipe::rm_group),
|
||||
)
|
||||
.route(
|
||||
"/recipe/group_name",
|
||||
patch(services::ron::recipe::set_group_name),
|
||||
)
|
||||
.route(
|
||||
"/recipe/group_comment",
|
||||
patch(services::ron::recipe::set_group_comment),
|
||||
)
|
||||
.route(
|
||||
"/recipe/groups_order",
|
||||
patch(services::ron::recipe::set_groups_order),
|
||||
)
|
||||
.route(
|
||||
"/recipe/step",
|
||||
post(services::ron::recipe::add_step).delete(services::ron::recipe::rm_step),
|
||||
)
|
||||
.route(
|
||||
"/recipe/step_action",
|
||||
patch(services::ron::recipe::set_step_action),
|
||||
)
|
||||
.route(
|
||||
"/recipe/steps_order",
|
||||
patch(services::ron::recipe::set_steps_order),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient",
|
||||
post(services::ron::recipe::add_ingredient)
|
||||
.delete(services::ron::recipe::rm_ingredient),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient_name",
|
||||
patch(services::ron::recipe::set_ingredient_name),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient_comment",
|
||||
patch(services::ron::recipe::set_ingredient_comment),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient_quantity",
|
||||
patch(services::ron::recipe::set_ingredient_quantity),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient_unit",
|
||||
patch(services::ron::recipe::set_ingredient_unit),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredients_order",
|
||||
patch(services::ron::recipe::set_ingredients_order),
|
||||
)
|
||||
.route(
|
||||
"/calendar/scheduled_recipes",
|
||||
get(services::ron::calendar::get_scheduled_recipes),
|
||||
)
|
||||
.route(
|
||||
"/calendar/scheduled_recipe",
|
||||
post(services::ron::calendar::add_scheduled_recipe)
|
||||
.delete(services::ron::calendar::rm_scheduled_recipe),
|
||||
)
|
||||
.route("/shopping_list", get(services::ron::shopping_list::get))
|
||||
.route(
|
||||
"/shopping_list/checked",
|
||||
patch(services::ron::shopping_list::set_entry_checked),
|
||||
)
|
||||
.fallback(services::ron::not_found);
|
||||
|
||||
let fragments_routes = Router::new().route(
|
||||
"/recipes_list",
|
||||
get(services::fragments::recipes_list_fragments),
|
||||
);
|
||||
|
||||
let html_routes_with_rate_limit = Router::new()
|
||||
.route("/signin", post(services::user::sign_in_post))
|
||||
.route("/signup", post(services::user::sign_up_post))
|
||||
.route(
|
||||
"/ask_reset_password",
|
||||
post(services::user::ask_reset_password_post),
|
||||
)
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|err: BoxError| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled error: {}", err),
|
||||
)
|
||||
}))
|
||||
.layer(BufferLayer::new(1024))
|
||||
.layer(RateLimitLayer::new(
|
||||
consts::NUMBER_OF_CONCURRENT_HTTP_REQUEST_FOR_RATE_LIMIT,
|
||||
consts::DURATION_FOR_RATE_LIMIT,
|
||||
)),
|
||||
);
|
||||
|
||||
let html_routes = Router::new()
|
||||
.route("/", get(services::home_page))
|
||||
.route("/dev_panel", get(services::dev_panel))
|
||||
.route("/logs", get(services::logs))
|
||||
.route("/signup", get(services::user::sign_up_get))
|
||||
.route("/validation", get(services::user::sign_up_validation))
|
||||
.route("/revalidation", get(services::user::email_revalidation))
|
||||
.route("/signin", get(services::user::sign_in_get))
|
||||
.route("/signout", get(services::user::sign_out))
|
||||
.route(
|
||||
"/ask_reset_password",
|
||||
get(services::user::ask_reset_password_get),
|
||||
)
|
||||
.route(
|
||||
"/reset_password",
|
||||
get(services::user::reset_password_get).post(services::user::reset_password_post),
|
||||
)
|
||||
// Recipes.
|
||||
.route("/recipe/new", get(services::recipe::create))
|
||||
.route("/recipe/edit/{id}", get(services::recipe::edit))
|
||||
.route("/recipe/view/{id}", get(services::recipe::view))
|
||||
// User.
|
||||
.route(
|
||||
"/user/edit",
|
||||
get(services::user::edit_user_get).post(services::user::edit_user_post),
|
||||
)
|
||||
.merge(html_routes_with_rate_limit)
|
||||
.nest("/fragments", fragments_routes)
|
||||
.route_layer(middleware::from_fn(services::ron_error_to_html));
|
||||
|
||||
let app = Router::new()
|
||||
.merge(html_routes)
|
||||
.nest("/ron-api", ron_api_routes)
|
||||
.fallback(services::not_found)
|
||||
.layer(middleware::from_fn_with_state(state.clone(), context))
|
||||
.with_state(state)
|
||||
.nest_service("/favicon.ico", ServeFile::new("static/favicon.ico"))
|
||||
.nest_service("/static", ServeDir::new("static"))
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
let url_rewriting_middleware = tower::util::MapRequestLayer::new(
|
||||
url_rewriting
|
||||
as fn(axum::http::Request<axum::body::Body>) -> axum::http::Request<axum::body::Body>,
|
||||
);
|
||||
|
||||
url_rewriting_middleware
|
||||
.layer(app)
|
||||
.into_make_service_with_connect_info::<SocketAddr>()
|
||||
}
|
||||
|
||||
fn url_rewriting(mut req: Request) -> Request {
|
||||
// Here we are extracting the language from the url then rewriting it.
|
||||
// For example:
|
||||
// "/fr/recipe/view/1"
|
||||
// lang = "fr" and uri rewritten as = "/recipe/view/1"
|
||||
let lang_and_new_uri = 'lang_and_new_uri: {
|
||||
if let Some(path_query) = req.uri().path_and_query() {
|
||||
let mut parts = path_query.path().split('/');
|
||||
let _ = parts.next(); // Empty part due to the first '/'.
|
||||
if let Some(lang) = parts.next() {
|
||||
let available_codes = translation::available_codes();
|
||||
if available_codes.contains(&lang) {
|
||||
let mut rest = String::from("");
|
||||
for part in parts {
|
||||
rest.push('/');
|
||||
rest.push_str(part);
|
||||
}
|
||||
if let Some(query) = path_query.query() {
|
||||
rest.push('?');
|
||||
rest.push_str(query);
|
||||
}
|
||||
|
||||
if let Ok(new_uri) = rest.parse::<Uri>() {
|
||||
break 'lang_and_new_uri Some((lang.to_string(), new_uri));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
if let Some((lang, new_uri)) = lang_and_new_uri {
|
||||
*req.uri_mut() = new_uri;
|
||||
req.extensions_mut().insert(Lang(Some(lang)));
|
||||
} else {
|
||||
req.extensions_mut().insert(Lang(None));
|
||||
}
|
||||
|
||||
req
|
||||
}
|
||||
|
||||
/// The language associated to the current HTTP request is defined in the current order:
|
||||
/// - Extraction from the url: like in `/fr/recipe/view/42`
|
||||
/// - Get from the user database record.
|
||||
/// - Get from the cookie.
|
||||
/// - Get from the HTTP header `accept-language`.
|
||||
/// - Set as `translation::DEFAULT_LANGUAGE_CODE`.
|
||||
async fn context(
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(lang_from_url): Extension<Lang>,
|
||||
mut req: Request,
|
||||
next: Next,
|
||||
) -> Result<Response> {
|
||||
let jar = CookieJar::from_headers(req.headers());
|
||||
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(req.headers(), addr);
|
||||
let user = get_current_user(connection, &jar, &client_ip, &client_user_agent).await;
|
||||
|
||||
let language = if let Some(lang) = lang_from_url.0 {
|
||||
lang
|
||||
} else if let Some(ref user) = user {
|
||||
user.lang.clone()
|
||||
} else {
|
||||
let available_codes = translation::available_codes();
|
||||
let jar = CookieJar::from_headers(req.headers());
|
||||
match jar.get(consts::COOKIE_LANG_NAME) {
|
||||
Some(lang) if available_codes.contains(&lang.value()) => lang.value().to_string(),
|
||||
_ => {
|
||||
let accept_language = req
|
||||
.headers()
|
||||
.get(axum::http::header::ACCEPT_LANGUAGE)
|
||||
.map(|v| v.to_str().unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(|l| l.split('-').next().unwrap_or_default())
|
||||
.find_or_first(|l| available_codes.contains(l));
|
||||
|
||||
match accept_language {
|
||||
Some(lang) if !lang.is_empty() => lang,
|
||||
_ => translation::DEFAULT_LANGUAGE_CODE,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let tr = Tr::new(&language);
|
||||
|
||||
let dark_theme = match jar.get(common::consts::COOKIE_DARK_THEME) {
|
||||
Some(dark_theme_cookie) => dark_theme_cookie.value().parse().unwrap_or_default(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
req.extensions_mut().insert(Context {
|
||||
user,
|
||||
tr,
|
||||
dark_theme,
|
||||
});
|
||||
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
|
||||
async fn get_current_user(
|
||||
connection: db::Connection,
|
||||
jar: &CookieJar,
|
||||
client_ip: &str,
|
||||
client_user_agent: &str,
|
||||
) -> Option<model::User> {
|
||||
match jar.get(consts::COOKIE_AUTH_TOKEN_NAME) {
|
||||
Some(token_cookie) => match connection
|
||||
.authentication(token_cookie.value(), client_ip, client_user_agent)
|
||||
.await
|
||||
{
|
||||
Ok(db::user::AuthenticationResult::NotValidToken) => None,
|
||||
Ok(db::user::AuthenticationResult::Ok(user_id)) => {
|
||||
match connection.load_user(user_id).await {
|
||||
Ok(user) => user,
|
||||
Err(error) => {
|
||||
event!(Level::WARN, "Error during authentication: {}", error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
event!(Level::WARN, "Error during authentication: {}", error);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use async_compression::tokio::bufread::GzipEncoder;
|
||||
use chrono::{NaiveTime, TimeDelta};
|
||||
use tokio::{fs::File, io::BufReader};
|
||||
|
|
@ -5,19 +7,103 @@ use tracing::{Level, event};
|
|||
|
||||
use super::db;
|
||||
|
||||
/// This function starts a backup process that runs at a specified time of day forever.
|
||||
/// This function starts a backup task that runs at a specified time of day forever.
|
||||
/// It creates a backup of the database at the specified directory.
|
||||
/// The backup file is named with the date and time at the time of the backup.
|
||||
pub fn start<P>(
|
||||
pub fn start_task<P>(
|
||||
directory: P,
|
||||
db_connection: db::Connection,
|
||||
time_of_day: NaiveTime,
|
||||
) -> tokio::task::JoinHandle<()>
|
||||
where
|
||||
P: AsRef<std::path::Path> + Send + Sync + 'static,
|
||||
{
|
||||
create_directory(&directory);
|
||||
let path = build_path(&directory);
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
loop {
|
||||
let mut time_to_wait = time_of_day - chrono::Local::now().time();
|
||||
if time_to_wait < TimeDelta::zero() {
|
||||
time_to_wait += TimeDelta::days(1);
|
||||
}
|
||||
event!(Level::DEBUG, "Backup in {}s", time_to_wait.num_seconds());
|
||||
tokio::time::sleep(time_to_wait.to_std().unwrap()).await;
|
||||
|
||||
start(&db_connection, &path).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Return a result (modify `start`)
|
||||
pub async fn do_backup<P>(db_connection: &db::Connection, directory: P)
|
||||
where
|
||||
P: AsRef<std::path::Path> + Send + Sync + 'static,
|
||||
{
|
||||
create_directory(&directory);
|
||||
start(db_connection, &build_path(&directory)).await
|
||||
}
|
||||
|
||||
async fn start(db_connection: &db::Connection, path: &PathBuf) {
|
||||
let path_compressed = path.with_extension("sqlite.gz");
|
||||
event!(
|
||||
Level::INFO,
|
||||
"Starting backup process to {}...",
|
||||
path_compressed.display()
|
||||
);
|
||||
|
||||
if let Err(error) = db_connection.backup(&path).await {
|
||||
event!(Level::ERROR, "Error when backing up database: {}", error);
|
||||
}
|
||||
|
||||
// Compress the backup file.
|
||||
match File::open(&path).await {
|
||||
Ok(file_input) => {
|
||||
let buf_reader = BufReader::new(file_input);
|
||||
let mut encoder = GzipEncoder::new(buf_reader);
|
||||
match File::create(&path_compressed).await {
|
||||
Ok(mut file_output) => {
|
||||
if let Err(error) = tokio::io::copy(&mut encoder, &mut file_output).await {
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"Error when compressing backup file: {}",
|
||||
error
|
||||
);
|
||||
} else if let Err(error) = std::fs::remove_file(path) {
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"Error when removing uncompressed backup file: {}",
|
||||
error
|
||||
);
|
||||
} else {
|
||||
event!(Level::INFO, "Backup done: {}", path_compressed.display());
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"Error when creating compressed backup file: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"Error when opening backup file for compression: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_directory<P>(directory: &P)
|
||||
where
|
||||
P: AsRef<std::path::Path> + Send + Sync + 'static,
|
||||
{
|
||||
if !directory.as_ref().exists() {
|
||||
std::fs::DirBuilder::new().create(&directory).unwrap();
|
||||
std::fs::DirBuilder::new().create(directory).unwrap();
|
||||
}
|
||||
|
||||
if !directory.as_ref().is_dir() {
|
||||
|
|
@ -29,74 +115,14 @@ where
|
|||
.unwrap_or("<Unable to convert directory to string>")
|
||||
);
|
||||
}
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
loop {
|
||||
let mut time_to_wait = time_of_day - chrono::Local::now().time();
|
||||
if time_to_wait < TimeDelta::zero() {
|
||||
time_to_wait += TimeDelta::days(1);
|
||||
}
|
||||
event!(Level::DEBUG, "Backup in {}s", time_to_wait.num_seconds());
|
||||
tokio::time::sleep(time_to_wait.to_std().unwrap()).await;
|
||||
|
||||
let path = directory.as_ref().join(format!(
|
||||
"recipes_backup_{}.sqlite",
|
||||
chrono::Local::now().format("%Y-%m-%d_%H%M%S")
|
||||
));
|
||||
let path_compressed = path.with_extension("sqlite.gz");
|
||||
|
||||
event!(
|
||||
Level::INFO,
|
||||
"Starting backup process to {}...",
|
||||
path_compressed.display()
|
||||
);
|
||||
|
||||
if let Err(error) = db_connection.backup(&path).await {
|
||||
event!(Level::ERROR, "Error when backing up database: {}", error);
|
||||
}
|
||||
|
||||
// Compress the backup file.
|
||||
match File::open(&path).await {
|
||||
Ok(file_input) => {
|
||||
let buf_reader = BufReader::new(file_input);
|
||||
let mut encoder = GzipEncoder::new(buf_reader);
|
||||
match File::create(&path_compressed).await {
|
||||
Ok(mut file_output) => {
|
||||
if let Err(error) =
|
||||
tokio::io::copy(&mut encoder, &mut file_output).await
|
||||
{
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"Error when compressing backup file: {}",
|
||||
error
|
||||
);
|
||||
} else if let Err(error) = std::fs::remove_file(&path) {
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"Error when removing uncompressed backup file: {}",
|
||||
error
|
||||
);
|
||||
} else {
|
||||
event!(Level::INFO, "Backup done: {}", path_compressed.display());
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"Error when creating compressed backup file: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
event!(
|
||||
Level::ERROR,
|
||||
"Error when opening backup file for compression: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn build_path<P>(directory: &P) -> PathBuf
|
||||
where
|
||||
P: AsRef<std::path::Path> + Send + Sync + 'static,
|
||||
{
|
||||
directory.as_ref().join(format!(
|
||||
"recipes_backup_{}.sqlite",
|
||||
chrono::Local::now().format("%Y-%m-%d_%H%M%S")
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use askama::Template;
|
||||
|
||||
use crate::{
|
||||
Context,
|
||||
app::Context,
|
||||
data::{db, model},
|
||||
log::Log,
|
||||
translation::{self, Sentence, Tr},
|
||||
|
|
|
|||
14
backend/src/lib.rs
Normal file
14
backend/src/lib.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub mod app;
|
||||
pub mod config;
|
||||
pub mod consts;
|
||||
pub mod data;
|
||||
pub mod log;
|
||||
|
||||
mod email;
|
||||
mod hash;
|
||||
mod html_templates;
|
||||
mod ron_extractor;
|
||||
mod ron_utils;
|
||||
mod services;
|
||||
mod translation;
|
||||
mod utils;
|
||||
|
|
@ -2,132 +2,35 @@
|
|||
|
||||
use std::{net::SocketAddr, path::Path};
|
||||
|
||||
use axum::{
|
||||
BoxError, Router, ServiceExt,
|
||||
error_handling::HandleErrorLayer,
|
||||
extract::{ConnectInfo, Extension, FromRef, Request, State},
|
||||
http::{StatusCode, Uri},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::{delete, get, patch, post, put},
|
||||
};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use chrono::prelude::*;
|
||||
use clap::Parser;
|
||||
use config::Config;
|
||||
use itertools::Itertools;
|
||||
use tokio::signal;
|
||||
use tower::layer::Layer;
|
||||
use tower::{ServiceBuilder, buffer::BufferLayer, limit::RateLimitLayer};
|
||||
use tower_http::{
|
||||
services::{ServeDir, ServeFile},
|
||||
trace::TraceLayer,
|
||||
};
|
||||
use tracing::{Level, event};
|
||||
|
||||
use data::{backup, db, model};
|
||||
use log::Log;
|
||||
use translation::Tr;
|
||||
use recipes::{
|
||||
app, config, consts,
|
||||
data::{backup, db},
|
||||
log::Log,
|
||||
};
|
||||
|
||||
mod config;
|
||||
mod consts;
|
||||
mod data;
|
||||
mod email;
|
||||
mod hash;
|
||||
mod html_templates;
|
||||
mod log;
|
||||
mod ron_extractor;
|
||||
mod ron_utils;
|
||||
mod services;
|
||||
mod translation;
|
||||
mod utils;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
config: Config,
|
||||
db_connection: db::Connection,
|
||||
log: Log,
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for Config {
|
||||
fn from_ref(app_state: &AppState) -> Config {
|
||||
app_state.config.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for db::Connection {
|
||||
fn from_ref(app_state: &AppState) -> db::Connection {
|
||||
app_state.db_connection.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for Log {
|
||||
fn from_ref(app_state: &AppState) -> Log {
|
||||
app_state.log.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::response::IntoResponse for db::DBError {
|
||||
fn into_response(self) -> Response {
|
||||
ron_utils::ron_error(StatusCode::INTERNAL_SERVER_ERROR, &self.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum AppError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] db::DBError),
|
||||
|
||||
#[error("Template error: {0}")]
|
||||
Render(#[from] askama::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, AppError>;
|
||||
|
||||
impl axum::response::IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Context {
|
||||
user: Option<model::User>,
|
||||
tr: Tr,
|
||||
dark_theme: bool,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn first_day_of_the_week(&self) -> Weekday {
|
||||
if let Some(user) = &self.user {
|
||||
user.first_day_of_the_week
|
||||
} else {
|
||||
self.tr.first_day_of_week()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should main returns 'Result'?
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = config::load();
|
||||
let log = Log::new(&config.logs_directory);
|
||||
|
||||
event!(Level::INFO, "Configuration: {:?}", config);
|
||||
|
||||
if !process_args(&config.database_directory).await {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
event!(Level::INFO, "Starting Recipes as web server...");
|
||||
|
||||
let Ok(db_connection) = db::Connection::new(&config.database_directory).await else {
|
||||
event!(Level::ERROR, "Unable to connect to the database");
|
||||
return;
|
||||
};
|
||||
let db_connection = db::Connection::new(&config.database_directory)
|
||||
.await
|
||||
.inspect_err(|err| event!(Level::ERROR, "Unable to connect to the database: {}", err))?;
|
||||
|
||||
if let Some(backup_time) = config.backup_time {
|
||||
backup::start(
|
||||
backup::start_task(
|
||||
config.backups_directory.clone(),
|
||||
db_connection.clone(),
|
||||
backup_time,
|
||||
|
|
@ -138,336 +41,24 @@ async fn main() {
|
|||
|
||||
let port = config.port;
|
||||
|
||||
let state = AppState {
|
||||
let state = app::AppState {
|
||||
config,
|
||||
db_connection,
|
||||
log,
|
||||
};
|
||||
|
||||
let ron_api_routes = Router::new()
|
||||
// Disabled: update user profile is now made with a post data ('edit_user_post').
|
||||
// .route("/user/update", put(services::ron::update_user))
|
||||
.route("/lang", put(services::ron::set_lang))
|
||||
.route("/recipe/titles", get(services::ron::recipe::get_titles))
|
||||
.route("/recipe/title", patch(services::ron::recipe::set_title))
|
||||
.route(
|
||||
"/recipe/description",
|
||||
patch(services::ron::recipe::set_description),
|
||||
)
|
||||
.route(
|
||||
"/recipe/servings",
|
||||
patch(services::ron::recipe::set_servings),
|
||||
)
|
||||
.route(
|
||||
"/recipe/estimated_time",
|
||||
patch(services::ron::recipe::set_estimated_time),
|
||||
)
|
||||
.route(
|
||||
"/recipe/tags",
|
||||
get(services::ron::recipe::get_tags)
|
||||
.post(services::ron::recipe::add_tags)
|
||||
.delete(services::ron::recipe::rm_tags),
|
||||
)
|
||||
.route(
|
||||
"/recipe/difficulty",
|
||||
patch(services::ron::recipe::set_difficulty),
|
||||
)
|
||||
.route(
|
||||
"/recipe/language",
|
||||
patch(services::ron::recipe::set_language),
|
||||
)
|
||||
.route(
|
||||
"/recipe/is_public",
|
||||
patch(services::ron::recipe::set_is_public),
|
||||
)
|
||||
.route("/recipe", delete(services::ron::recipe::rm))
|
||||
.route("/recipe/groups", get(services::ron::recipe::get_groups))
|
||||
.route(
|
||||
"/recipe/group",
|
||||
post(services::ron::recipe::add_group).delete(services::ron::recipe::rm_group),
|
||||
)
|
||||
.route(
|
||||
"/recipe/group_name",
|
||||
patch(services::ron::recipe::set_group_name),
|
||||
)
|
||||
.route(
|
||||
"/recipe/group_comment",
|
||||
patch(services::ron::recipe::set_group_comment),
|
||||
)
|
||||
.route(
|
||||
"/recipe/groups_order",
|
||||
patch(services::ron::recipe::set_groups_order),
|
||||
)
|
||||
.route(
|
||||
"/recipe/step",
|
||||
post(services::ron::recipe::add_step).delete(services::ron::recipe::rm_step),
|
||||
)
|
||||
.route(
|
||||
"/recipe/step_action",
|
||||
patch(services::ron::recipe::set_step_action),
|
||||
)
|
||||
.route(
|
||||
"/recipe/steps_order",
|
||||
patch(services::ron::recipe::set_steps_order),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient",
|
||||
post(services::ron::recipe::add_ingredient)
|
||||
.delete(services::ron::recipe::rm_ingredient),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient_name",
|
||||
patch(services::ron::recipe::set_ingredient_name),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient_comment",
|
||||
patch(services::ron::recipe::set_ingredient_comment),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient_quantity",
|
||||
patch(services::ron::recipe::set_ingredient_quantity),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredient_unit",
|
||||
patch(services::ron::recipe::set_ingredient_unit),
|
||||
)
|
||||
.route(
|
||||
"/recipe/ingredients_order",
|
||||
patch(services::ron::recipe::set_ingredients_order),
|
||||
)
|
||||
.route(
|
||||
"/calendar/scheduled_recipes",
|
||||
get(services::ron::calendar::get_scheduled_recipes),
|
||||
)
|
||||
.route(
|
||||
"/calendar/scheduled_recipe",
|
||||
post(services::ron::calendar::add_scheduled_recipe)
|
||||
.delete(services::ron::calendar::rm_scheduled_recipe),
|
||||
)
|
||||
.route("/shopping_list", get(services::ron::shopping_list::get))
|
||||
.route(
|
||||
"/shopping_list/checked",
|
||||
patch(services::ron::shopping_list::set_entry_checked),
|
||||
)
|
||||
.fallback(services::ron::not_found);
|
||||
|
||||
let fragments_routes = Router::new().route(
|
||||
"/recipes_list",
|
||||
get(services::fragments::recipes_list_fragments),
|
||||
);
|
||||
|
||||
let html_routes_with_rate_limit = Router::new()
|
||||
.route("/signin", post(services::user::sign_in_post))
|
||||
.route("/signup", post(services::user::sign_up_post))
|
||||
.route(
|
||||
"/ask_reset_password",
|
||||
post(services::user::ask_reset_password_post),
|
||||
)
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|err: BoxError| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled error: {}", err),
|
||||
)
|
||||
}))
|
||||
.layer(BufferLayer::new(1024))
|
||||
.layer(RateLimitLayer::new(
|
||||
consts::NUMBER_OF_CONCURRENT_HTTP_REQUEST_FOR_RATE_LIMIT,
|
||||
consts::DURATION_FOR_RATE_LIMIT,
|
||||
)),
|
||||
);
|
||||
|
||||
let html_routes = Router::new()
|
||||
.route("/", get(services::home_page))
|
||||
.route("/dev_panel", get(services::dev_panel))
|
||||
.route("/logs", get(services::logs))
|
||||
.route("/signup", get(services::user::sign_up_get))
|
||||
.route("/validation", get(services::user::sign_up_validation))
|
||||
.route("/revalidation", get(services::user::email_revalidation))
|
||||
.route("/signin", get(services::user::sign_in_get))
|
||||
.route("/signout", get(services::user::sign_out))
|
||||
.route(
|
||||
"/ask_reset_password",
|
||||
get(services::user::ask_reset_password_get),
|
||||
)
|
||||
.route(
|
||||
"/reset_password",
|
||||
get(services::user::reset_password_get).post(services::user::reset_password_post),
|
||||
)
|
||||
// Recipes.
|
||||
.route("/recipe/new", get(services::recipe::create))
|
||||
.route("/recipe/edit/{id}", get(services::recipe::edit))
|
||||
.route("/recipe/view/{id}", get(services::recipe::view))
|
||||
// User.
|
||||
.route(
|
||||
"/user/edit",
|
||||
get(services::user::edit_user_get).post(services::user::edit_user_post),
|
||||
)
|
||||
.merge(html_routes_with_rate_limit)
|
||||
.nest("/fragments", fragments_routes)
|
||||
.route_layer(middleware::from_fn(services::ron_error_to_html));
|
||||
|
||||
let app = Router::new()
|
||||
.merge(html_routes)
|
||||
.nest("/ron-api", ron_api_routes)
|
||||
.fallback(services::not_found)
|
||||
.layer(middleware::from_fn_with_state(state.clone(), context))
|
||||
.with_state(state)
|
||||
.nest_service("/favicon.ico", ServeFile::new("static/favicon.ico"))
|
||||
.nest_service("/static", ServeDir::new("static"))
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
let url_rewriting_middleware = tower::util::MapRequestLayer::new(url_rewriting);
|
||||
let app_with_url_rewriting = url_rewriting_middleware.layer(app);
|
||||
let make_service = app::make_service(state);
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
|
||||
axum::serve(
|
||||
listener,
|
||||
app_with_url_rewriting.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await
|
||||
.unwrap();
|
||||
axum::serve(listener, make_service)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
|
||||
event!(Level::INFO, "Recipes stopped");
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Lang(Option<String>);
|
||||
|
||||
fn url_rewriting(mut req: Request) -> Request {
|
||||
// Here we are extracting the language from the url then rewriting it.
|
||||
// For example:
|
||||
// "/fr/recipe/view/1"
|
||||
// lang = "fr" and uri rewritten as = "/recipe/view/1"
|
||||
let lang_and_new_uri = 'lang_and_new_uri: {
|
||||
if let Some(path_query) = req.uri().path_and_query() {
|
||||
let mut parts = path_query.path().split('/');
|
||||
let _ = parts.next(); // Empty part due to the first '/'.
|
||||
if let Some(lang) = parts.next() {
|
||||
let available_codes = translation::available_codes();
|
||||
if available_codes.contains(&lang) {
|
||||
let mut rest = String::from("");
|
||||
for part in parts {
|
||||
rest.push('/');
|
||||
rest.push_str(part);
|
||||
}
|
||||
if let Some(query) = path_query.query() {
|
||||
rest.push('?');
|
||||
rest.push_str(query);
|
||||
}
|
||||
|
||||
if let Ok(new_uri) = rest.parse::<Uri>() {
|
||||
break 'lang_and_new_uri Some((lang.to_string(), new_uri));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
if let Some((lang, new_uri)) = lang_and_new_uri {
|
||||
*req.uri_mut() = new_uri;
|
||||
req.extensions_mut().insert(Lang(Some(lang)));
|
||||
} else {
|
||||
req.extensions_mut().insert(Lang(None));
|
||||
}
|
||||
|
||||
req
|
||||
}
|
||||
|
||||
/// The language associated to the current HTTP request is defined in the current order:
|
||||
/// - Extraction from the url: like in `/fr/recipe/view/42`
|
||||
/// - Get from the user database record.
|
||||
/// - Get from the cookie.
|
||||
/// - Get from the HTTP header `accept-language`.
|
||||
/// - Set as `translation::DEFAULT_LANGUAGE_CODE`.
|
||||
async fn context(
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(lang_from_url): Extension<Lang>,
|
||||
mut req: Request,
|
||||
next: Next,
|
||||
) -> Result<Response> {
|
||||
let jar = CookieJar::from_headers(req.headers());
|
||||
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(req.headers(), addr);
|
||||
let user = get_current_user(connection, &jar, &client_ip, &client_user_agent).await;
|
||||
|
||||
let language = if let Some(lang) = lang_from_url.0 {
|
||||
lang
|
||||
} else if let Some(ref user) = user {
|
||||
user.lang.clone()
|
||||
} else {
|
||||
let available_codes = translation::available_codes();
|
||||
let jar = CookieJar::from_headers(req.headers());
|
||||
match jar.get(consts::COOKIE_LANG_NAME) {
|
||||
Some(lang) if available_codes.contains(&lang.value()) => lang.value().to_string(),
|
||||
_ => {
|
||||
let accept_language = req
|
||||
.headers()
|
||||
.get(axum::http::header::ACCEPT_LANGUAGE)
|
||||
.map(|v| v.to_str().unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(|l| l.split('-').next().unwrap_or_default())
|
||||
.find_or_first(|l| available_codes.contains(l));
|
||||
|
||||
match accept_language {
|
||||
Some(lang) if !lang.is_empty() => lang,
|
||||
_ => translation::DEFAULT_LANGUAGE_CODE,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let tr = Tr::new(&language);
|
||||
|
||||
let dark_theme = match jar.get(common::consts::COOKIE_DARK_THEME) {
|
||||
Some(dark_theme_cookie) => dark_theme_cookie.value().parse().unwrap_or_default(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
req.extensions_mut().insert(Context {
|
||||
user,
|
||||
tr,
|
||||
dark_theme,
|
||||
});
|
||||
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
|
||||
async fn get_current_user(
|
||||
connection: db::Connection,
|
||||
jar: &CookieJar,
|
||||
client_ip: &str,
|
||||
client_user_agent: &str,
|
||||
) -> Option<model::User> {
|
||||
match jar.get(consts::COOKIE_AUTH_TOKEN_NAME) {
|
||||
Some(token_cookie) => match connection
|
||||
.authentication(token_cookie.value(), client_ip, client_user_agent)
|
||||
.await
|
||||
{
|
||||
Ok(db::user::AuthenticationResult::NotValidToken) => None,
|
||||
Ok(db::user::AuthenticationResult::Ok(user_id)) => {
|
||||
match connection.load_user(user_id).await {
|
||||
Ok(user) => user,
|
||||
Err(error) => {
|
||||
event!(Level::WARN, "Error during authentication: {}", error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
event!(Level::WARN, "Error during authentication: {}", error);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
|
|
@ -491,6 +82,7 @@ where
|
|||
|
||||
if args.dbtest {
|
||||
// Make a backup of the database.
|
||||
// TODO: use the `backup` module instead of copying the file manually.
|
||||
let db_path = database_directory.as_ref().join(consts::DB_FILENAME);
|
||||
if db_path.exists() {
|
||||
let db_path_bckup = (1..)
|
||||
|
|
@ -515,14 +107,6 @@ where
|
|||
if let Err(error) = con.execute_file("sql/data_test.sql").await {
|
||||
event!(Level::ERROR, "{}", error);
|
||||
}
|
||||
// Set the creation datetime to 'now'.
|
||||
con.execute_sql(
|
||||
sqlx::query(
|
||||
"UPDATE [User] SET [validation_token_datetime] = $1 WHERE [email] = 'paul@test.org'")
|
||||
.bind(Utc::now())
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
event!(
|
||||
Level::INFO,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ use axum::{
|
|||
use serde::Deserialize;
|
||||
// use tracing::{event, Level};
|
||||
|
||||
use crate::{Context, Result, data::db, html_templates::*};
|
||||
use crate::{
|
||||
app::{Context, Result},
|
||||
data::db,
|
||||
html_templates::*,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CurrentRecipeId {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,14 @@ use axum::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{AppState, Context, Result, consts, data::db, html_templates::*, log::Log, ron_utils};
|
||||
use crate::{
|
||||
app::{AppState, Context, Result},
|
||||
consts,
|
||||
data::db,
|
||||
html_templates::*,
|
||||
log::Log,
|
||||
ron_utils,
|
||||
};
|
||||
|
||||
pub mod fragments;
|
||||
pub mod recipe;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ use axum::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
Context, Result,
|
||||
data::{db, model},
|
||||
app::{Context, Result},
|
||||
data::db,
|
||||
html_templates::*,
|
||||
translation::Sentence,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ use axum::{
|
|||
use axum_extra::extract::Query;
|
||||
|
||||
use crate::{
|
||||
Context, consts,
|
||||
app::Context,
|
||||
consts,
|
||||
data::{self, db},
|
||||
ron_extractor::ExtractRon,
|
||||
ron_utils::{ron_error, ron_response_ok},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ use axum::{
|
|||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||
// use tracing::{event, Level};
|
||||
|
||||
use crate::{Context, consts, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error};
|
||||
use crate::{
|
||||
app::Context, consts, data::db, data::model, ron_extractor::ExtractRon, ron_utils::ron_error,
|
||||
};
|
||||
|
||||
pub mod calendar;
|
||||
pub mod recipe;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ use axum_extra::extract::Query;
|
|||
use common::ron_api;
|
||||
// use tracing::{event, Level};
|
||||
|
||||
use crate::{Context, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_response_ok};
|
||||
use crate::{
|
||||
app::Context, data::db, data::model, ron_extractor::ExtractRon, ron_utils::ron_response_ok,
|
||||
};
|
||||
|
||||
use super::rights::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use axum::{
|
|||
response::{ErrorResponse, Result},
|
||||
};
|
||||
|
||||
use crate::{consts, data::db, model, ron_utils::ron_error};
|
||||
use crate::{consts, data::db, data::model, ron_utils::ron_error};
|
||||
|
||||
pub async fn check_user_rights_recipe(
|
||||
connection: &db::Connection,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ use axum::{
|
|||
use common::ron_api;
|
||||
|
||||
use crate::{
|
||||
Context, consts,
|
||||
app::Context,
|
||||
consts,
|
||||
data::db,
|
||||
model,
|
||||
data::model,
|
||||
ron_extractor::ExtractRon,
|
||||
ron_utils::{ron_error, ron_response_ok},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,8 +20,14 @@ use strum_macros::Display;
|
|||
use tracing::{Level, event};
|
||||
|
||||
use crate::{
|
||||
AppState, Context, Result, config::Config, consts, data::db, email, html_templates::*,
|
||||
translation::Sentence, utils,
|
||||
app::{AppState, Context, Result},
|
||||
config::Config,
|
||||
consts,
|
||||
data::db,
|
||||
email,
|
||||
html_templates::*,
|
||||
translation::Sentence,
|
||||
utils,
|
||||
};
|
||||
|
||||
const VALIDATION_TOKEN_KEY: &str = "validation_token";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue