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 comments: String,
50
}
51

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

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

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

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

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

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