1
20
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
#![deny(
21
    //missing_docs,
22
    rustdoc::broken_intra_doc_links,
23
    /* groups */
24
    clippy::correctness,
25
    clippy::suspicious,
26
    clippy::complexity,
27
    clippy::perf,
28
    clippy::style,
29
    clippy::cargo,
30
    clippy::nursery,
31
    /* restriction */
32
    clippy::dbg_macro,
33
    clippy::rc_buffer,
34
    clippy::as_underscore,
35
    clippy::assertions_on_result_states,
36
    /* pedantic */
37
    clippy::cast_lossless,
38
    clippy::cast_possible_wrap,
39
    clippy::ptr_as_ptr,
40
    clippy::bool_to_int_with_if,
41
    clippy::borrow_as_ptr,
42
    clippy::case_sensitive_file_extension_comparisons,
43
    clippy::cast_lossless,
44
    clippy::cast_ptr_alignment,
45
    clippy::naive_bytecount
46
)]
47
#![allow(clippy::multiple_crate_versions, clippy::missing_const_for_fn)]
48

            
49
pub use axum::{
50
    extract::{Path, Query, State},
51
    handler::Handler,
52
    response::{Html, IntoResponse, Redirect},
53
    routing::{get, post},
54
    Extension, Form, Router,
55
};
56
pub use axum_extra::routing::TypedPath;
57
pub use axum_login::{
58
    memory_store::MemoryStore as AuthMemoryStore, secrecy::SecretVec, AuthLayer, AuthUser,
59
    RequireAuthorizationLayer,
60
};
61
pub use axum_sessions::{
62
    async_session::MemoryStore,
63
    extractors::{ReadableSession, WritableSession},
64
    SessionLayer,
65
};
66

            
67
pub type AuthContext =
68
    axum_login::extractors::AuthContext<i64, auth::User, Arc<AppState>, auth::Role>;
69

            
70
pub type RequireAuth = auth::auth_request::RequireAuthorizationLayer<i64, auth::User, auth::Role>;
71

            
72
pub use std::result::Result;
73
use std::{borrow::Cow, collections::HashMap, sync::Arc};
74

            
75
use chrono::Datelike;
76
pub use http::{Request, Response, StatusCode};
77
use mailpot::{models::DbVal, rusqlite::OptionalExtension, *};
78
use minijinja::{
79
    value::{Object, Value},
80
    Environment, Error,
81
};
82
use tokio::sync::RwLock;
83

            
84
pub mod auth;
85
pub mod cal;
86
pub mod help;
87
pub mod lists;
88
pub mod minijinja_utils;
89
pub mod settings;
90
pub mod topics;
91
pub mod typed_paths;
92
pub mod utils;
93

            
94
pub use auth::*;
95
pub use cal::{calendarize, *};
96
pub use help::*;
97
pub use lists::{
98
    list, list_candidates, list_edit, list_edit_POST, list_post, list_post_eml, list_post_raw,
99
    list_subscribers, PostPolicySettings, SubscriptionPolicySettings,
100
};
101
pub use minijinja_utils::*;
102
pub use settings::{
103
    settings, settings_POST, user_list_subscription, user_list_subscription_POST,
104
    SubscriptionFormPayload,
105
};
106
pub use topics::*;
107
pub use typed_paths::{tsr::RouterExt, *};
108
pub use utils::*;
109

            
110
#[derive(Debug)]
111
pub struct ResponseError {
112
    pub inner: Box<dyn std::error::Error>,
113
    pub status: StatusCode,
114
}
115

            
116
impl std::fmt::Display for ResponseError {
117
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
118
        write!(fmt, "Inner: {}, status: {}", self.inner, self.status)
119
    }
120
}
121

            
122
impl ResponseError {
123
3
    pub fn new(msg: String, status: StatusCode) -> Self {
124
3
        Self {
125
3
            inner: Box::<dyn std::error::Error + Send + Sync>::from(msg),
126
            status,
127
        }
128
3
    }
129
}
130

            
131
impl<E: Into<Box<dyn std::error::Error>>> From<E> for ResponseError {
132
    fn from(err: E) -> Self {
133
        Self {
134
            inner: err.into(),
135
            status: StatusCode::INTERNAL_SERVER_ERROR,
136
        }
137
    }
138
}
139

            
140
pub trait IntoResponseError {
141
    fn with_status(self, status: StatusCode) -> ResponseError;
142
}
143

            
144
impl<E: Into<Box<dyn std::error::Error>>> IntoResponseError for E {
145
    fn with_status(self, status: StatusCode) -> ResponseError {
146
        ResponseError {
147
            status,
148
            ..ResponseError::from(self)
149
        }
150
    }
151
}
152

            
153
impl IntoResponse for ResponseError {
154
3
    fn into_response(self) -> axum::response::Response {
155
3
        let Self { inner, status } = self;
156
6
        (status, inner.to_string()).into_response()
157
3
    }
158
}
159

            
160
pub trait IntoResponseErrorResult<R> {
161
    fn with_status(self, status: StatusCode) -> std::result::Result<R, ResponseError>;
162
}
163

            
164
impl<R, E> IntoResponseErrorResult<R> for std::result::Result<R, E>
165
where
166
    E: IntoResponseError,
167
{
168
    fn with_status(self, status: StatusCode) -> std::result::Result<R, ResponseError> {
169
        self.map_err(|err| err.with_status(status))
170
    }
171
}
172

            
173
#[derive(Clone)]
174
pub struct AppState {
175
    pub conf: Configuration,
176
    pub root_url_prefix: Value,
177
    pub public_url: String,
178
    pub site_title: Cow<'static, str>,
179
    pub site_subtitle: Option<Cow<'static, str>>,
180
    pub user_store: Arc<RwLock<HashMap<i64, User>>>,
181
    // ...
182
}
183

            
184
mod auth_impls {
185
    use super::*;
186
    type UserId = i64;
187
    type User = auth::User;
188
    type Role = auth::Role;
189

            
190
    impl AppState {
191
4
        pub async fn insert_user(&self, pk: UserId, user: User) {
192
            self.user_store.write().await.insert(pk, user);
193
4
        }
194
    }
195

            
196
    #[axum::async_trait]
197
    impl axum_login::UserStore<UserId, Role> for Arc<AppState>
198
    where
199
        User: axum_login::AuthUser<UserId, Role>,
200
    {
201
        type User = User;
202

            
203
6
        async fn load_user(
204
6
            &self,
205
            user_id: &UserId,
206
18
        ) -> std::result::Result<Option<Self::User>, eyre::Report> {
207
6
            Ok(self.user_store.read().await.get(user_id).cloned())
208
24
        }
209
    }
210
}
211

            
212
const fn _get_package_git_sha() -> Option<&'static str> {
213
    option_env!("PACKAGE_GIT_SHA")
214
}
215

            
216
const _PACKAGE_COMMIT_SHA: Option<&str> = _get_package_git_sha();
217

            
218
pub fn get_git_sha() -> std::borrow::Cow<'static, str> {
219
    if let Some(r) = _PACKAGE_COMMIT_SHA {
220
        return r.into();
221
    }
222
    build_info::build_info!(fn build_info);
223
    let info = build_info();
224
    info.version_control
225
        .as_ref()
226
        .and_then(|v| v.git())
227
        .map(|g| g.commit_short_id.clone())
228
        .map_or_else(|| "<unknown>".into(), |v| v.into())
229
}
230

            
231
pub const VERSION_INFO: &str = build_info::format!("{}", $.crate_info.version);
232
pub const BUILD_INFO: &str = build_info::format!("{}\t{}\t{}\t{}", $.crate_info.version, $.compiler, $.timestamp, $.crate_info.enabled_features);
233
pub const CLI_INFO: &str = build_info::format!("{} Version: {}\nAuthors: {}\nLicense: AGPL version 3 or later\nCompiler: {}\nBuild-Date: {}\nEnabled-features: {}", $.crate_info.name, $.crate_info.version, $.crate_info.authors, $.compiler, $.timestamp, $.crate_info.enabled_features);