1
mod auth;
2
mod config;
3
pub mod data;
4
mod data_format;
5
pub mod error;
6
pub mod handlers;
7
mod mail_config;
8
mod mailer;
9
mod util;
10

            
11
use crate::auth::Claims;
12
use actix_http::HttpMessage;
13
use actix_web::{
14
    dev::ServiceRequest,
15
    error::ErrorBadRequest,
16
    web::{self, Json},
17
    Responder, Result,
18
};
19
use actix_web_httpauth::extractors::bearer::BearerAuth;
20
use auth::validate_token;
21
use chrono::Utc;
22
use data::Data;
23
use data_format::ReviewRequest;
24
use jsonwebtoken::{encode, EncodingKey, Header};
25
use serde::{Deserialize, Serialize};
26
use serde_json::Value;
27
use std::{collections::HashMap, path::PathBuf};
28

            
29
30
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
30
#[serde(deny_unknown_fields)]
31
pub struct ReviewRequestForm {
32
    pub projectname: String,
33
    pub name: String,
34
    pub phone: Option<String>,
35
    pub email: String,
36
    pub programme: String,
37
    pub summary: String,
38
    pub www: String,
39
    pub i18n: Option<String>,
40
    pub licensing: Option<String>,
41
    pub testing: Option<String>,
42
    pub community: Option<String>,
43
    pub a11y: Option<String>,
44
    pub packaging: Option<String>,
45
    pub diversity: Option<String>,
46
    pub standards: Option<String>,
47
    pub security: Option<String>,
48
    pub governance: Option<String>,
49
    pub uxtesting: Option<String>,
50
    pub technicalwriting: Option<String>,
51
    pub comments: String,
52
}
53

            
54
6
fn form_to_request(form: ReviewRequestForm) -> Result<ReviewRequest> {
55
6
    use actix_web::error;
56
6
    if form.projectname.is_empty() {
57
3
        return Err(error::ErrorBadRequest("projectname is empty"));
58
3
    }
59
3
    if form.name.is_empty() {
60
        return Err(error::ErrorBadRequest("name is empty"));
61
3
    }
62
3
    if form.email.is_empty() {
63
        return Err(error::ErrorBadRequest("email is empty"));
64
3
    }
65
3
    if form.programme.is_empty() {
66
        return Err(error::ErrorBadRequest("programme is empty"));
67
3
    }
68
3
    if form.www.is_empty() {
69
        return Err(error::ErrorBadRequest("www is empty"));
70
3
    }
71
3
    Ok(ReviewRequest {
72
3
        date: Utc::now(),
73
3
        projectname: form.projectname,
74
3
        name: form.name,
75
3
        phone: form.phone,
76
3
        email: form.email,
77
3
        programme: form.programme,
78
3
        www: form.www,
79
3
        summary: form.summary,
80
3
        i18n: form.i18n.is_some(),
81
3
        licensing: form.licensing.is_some(),
82
3
        testing: form.testing.is_some(),
83
3
        community: form.community.is_some(),
84
3
        a11y: form.a11y.is_some(),
85
3
        packaging: form.packaging.is_some(),
86
3
        diversity: form.diversity.is_some(),
87
3
        standards: form.standards.is_some(),
88
3
        security: form.security.is_some(),
89
3
        governance: form.governance.is_some(),
90
3
        uxtesting: form.uxtesting.is_some(),
91
3
        technicalwriting: form.technicalwriting.is_some(),
92
3
        comments: form.comments,
93
3
    })
94
6
}
95

            
96
// Handler for the web form
97
6
pub async fn form_cgi(
98
6
    form: web::Form<ReviewRequestForm>,
99
6
    data: web::Data<Data>,
100
6
) -> Result<impl Responder> {
101
2
    let request = form_to_request(form.into_inner())?;
102
1
    if !util::check_websites(&request.www) {
103
        return Err(ErrorBadRequest(
104
            "One or more of the websites are not valid.",
105
        ));
106
1
    }
107
1
    let json = serde_json::to_string_pretty(&request)?;
108
1
    let _request = data.add_request(json.as_bytes()).await?;
109
1
    Ok("your form was submitted!")
110
2
}
111

            
112
// Function for test_mode to get a jwt token.
113
pub async fn get_jwt(claims: Json<Value>, data: web::Data<Data>) -> Result<impl Responder> {
114
    let header = encode(
115
        &Header::default(),
116
        &claims,
117
        &EncodingKey::from_secret(data.jwks().secret()),
118
    )
119
    .unwrap();
120
    let mut token = HashMap::new();
121
    token.insert("token".to_string(), header);
122
    Ok(Json(token))
123
}
124

            
125
/*
126
 * ```
127
 * curl -v --header "Authorization: Bearer testing" http://0.0.0.0:8085/api/requests
128
 * ```
129
 */
130
pub async fn validator(
131
    req: ServiceRequest,
132
    credentials: BearerAuth,
133
    data: web::Data<Data>,
134
) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
135
    use actix_web::error;
136
    match validate_token(credentials.token(), data.jwks().secret()) {
137
        Ok(claims) => {
138
            req.extensions_mut().insert::<Claims>(claims);
139
            Ok(req)
140
        }
141
        Err(e) => Err((error::ErrorBadRequest(e), req)),
142
    }
143
}
144

            
145
// return 'static' from the working directory if that is a dir and otherwise
146
// use the installed version
147
9
pub fn get_static_dir() -> PathBuf {
148
9
    let dir = PathBuf::from("static");
149
9
    if dir.is_dir() && dir.join("app.js").exists() {
150
9
        return dir;
151
    }
152
    let dir = std::env::current_exe()
153
        .unwrap()
154
        .parent()
155
        .unwrap()
156
        .parent()
157
        .unwrap()
158
        .join("share/static");
159
    if !dir.is_dir() {
160
        panic!("There is no 'static' directory.");
161
    }
162
    dir
163
9
}
164

            
165
#[macro_export]
166
macro_rules! app (
167
    ($data: expr, $cors: expr) => ({
168
        use ngi0review::{validator, handlers::*};
169
        use actix_files::Files;
170
        use actix_web::middleware::Logger;
171
        let data_clone = $data.clone();
172
        let auth = actix_web_httpauth::middleware::HttpAuthentication::bearer(
173
            move |req, credentials|
174
                validator(req, credentials, data_clone.clone()));
175
        let cors = actix_cors::Cors::default()
176
            .allow_any_origin()
177
            .allowed_methods(["GET"]);
178
        let test_mode = $data.config().test_mode;
179
        let app = App::new()
180
            .wrap(Logger::default())
181
            .app_data($data.clone())
182
            .service(web::scope("/static").wrap(cors).service(Files::new("", $crate::get_static_dir())))
183
            .service(Files::new("/files", $data.data_path().join("files")).disable_content_disposition())
184
            // receive the posted forms
185
            .service(web::scope("/NGI0/review").route("/form.cgi", web::post().to(ngi0review::form_cgi)))
186
            .service(web::scope("/api")
187
                .wrap(auth)
188
                .wrap($cors)
189
                .route("/selectors", web::get().to(get_selectors))
190
                .route("/review-types", web::get().to(get_review_types))
191
                .route("/rejections", web::get().to(get_rejections))
192
                .route("/rejections", web::post().to(add_rejection))
193
                .route("/requests", web::get().to(get_requests))
194
                .route("/requests", web::post().to(add_request))
195
                .route("/tasks", web::get().to(get_tasks))
196
                .route("/tasks", web::post().to(add_task))
197
                .route("/reports", web::get().to(get_reports))
198
                .route("/reports", web::post().to(add_report))
199
                .route("/files", web::post().to(add_file)));
200
        if test_mode {
201
            // add route to hand out jwt tokens
202
            app.route("/jwt", web::post().to(ngi0review::get_jwt))
203
        } else {
204
            app
205
        }
206
    });
207
);