corund_lib/
lib.rs

1use std::{env::var, fs::File, io::Read};
2
3use axum::{
4    extract::Request,
5    http::HeaderMap,
6    middleware::{self, Next},
7    response::{IntoResponse, Response},
8    routing::post,
9    Json, Router,
10};
11use diesel::prelude::Insertable;
12use password::check_password;
13use quco::Query;
14use ryz::{
15    dict::dict,
16    err::{self, Error},
17    path,
18    res::Res,
19    time::Time,
20};
21use serde::{Deserialize, Serialize};
22use token::{new_at, verify_rt};
23use user::{get_by_id, get_by_rt, User};
24use user_change::UserChange;
25
26pub mod db;
27mod password;
28pub mod quco;
29pub mod ryz;
30mod schema;
31pub mod token;
32pub mod user;
33pub mod user_change;
34
35lazy_static::lazy_static! {
36    static ref APPRC: Apprc = get_apprc();
37}
38
39fn get_apprc() -> Apprc {
40    let mut file =
41        File::open(path::cwd().unwrap().join("corund.cfg.yml")).unwrap();
42    let mut content = String::new();
43    file.read_to_string(&mut content).unwrap();
44    let mut mode_to_apprc: dict<String, Apprc> =
45        serde_yml::from_str(&content).unwrap();
46    let mode = get_mode();
47    mode_to_apprc.remove(&mode).unwrap()
48}
49
50pub fn get_mode() -> String {
51    return match var("CORUND_MODE") {
52        Err(_) => "prod".to_string(),
53        Ok(mode) => mode,
54    };
55}
56
57#[derive(Debug, Deserialize)]
58struct Apprc {
59    sql: SqlCfg,
60    domain: DomainCfg,
61}
62
63#[derive(Debug, Deserialize)]
64struct DomainCfg {
65    secret: String,
66}
67
68#[derive(Debug, Deserialize)]
69struct SqlCfg {
70    url: String,
71    is_cleaning_allowed: bool,
72}
73
74#[derive(Deserialize)]
75struct Login {
76    username: String,
77    password: String,
78}
79
80#[derive(Deserialize)]
81struct RtData {
82    rt: String,
83}
84
85#[derive(Serialize, Deserialize)]
86pub struct Reg {
87    pub username: String,
88    pub password: String,
89    pub firstname: Option<String>,
90    pub patronym: Option<String>,
91    pub surname: Option<String>,
92}
93
94#[derive(Insertable)]
95#[diesel(table_name=schema::appuser)]
96pub struct InsertReg {
97    pub username: String,
98    pub hpassword: String,
99    pub firstname: Option<String>,
100    pub patronym: Option<String>,
101    pub surname: Option<String>,
102}
103
104#[derive(Deserialize)]
105struct GetChanges {
106    from: Time,
107}
108
109async fn err_middleware(req: Request, next: Next) -> Response {
110    let res = next.run(req).await;
111
112    // TODO: handle status codes for errs somehow without knowing response
113    //       body type... don't even know how, for now
114
115    res
116}
117
118impl IntoResponse for Error {
119    fn into_response(self) -> Response {
120        Json(self).into_response()
121    }
122}
123
124async fn rpc_reg(headers: HeaderMap, Json(reg): Json<Reg>) -> Res<Json<User>> {
125    verify_domain_secret_from_headers(headers)?;
126    let con = &mut db::con().unwrap();
127    let user = user::new(&reg, con).unwrap();
128    Ok(Json(user))
129}
130
131async fn rpc_dereg(headers: HeaderMap, Json(query): Json<Query>) {
132    verify_domain_secret_from_headers(headers).unwrap();
133    let con = &mut db::con().unwrap();
134    user::del(&query, con).unwrap();
135}
136
137/// Logins an user into the system.
138///
139/// All other login sessions are discarded (only 1 refresh token is possible
140/// by default).
141///
142/// Returns refresh token.
143async fn rpc_login(Json(login): Json<Login>) -> Res<String> {
144    let con = &mut db::con().unwrap();
145    let Ok((user, hpassword)) = user::get_by_username(&login.username, con)
146    else {
147        return err::res_msg(
148            format!("invalid username {}", login.username.to_owned()).as_str(),
149        );
150    };
151    if !check_password(&login.password, &hpassword) {
152        return err::res_msg("incorrect password");
153    }
154    let rt = token::new_rt(user.id).unwrap();
155    let con = &mut db::con().unwrap();
156    user::set_rt_for_username(&login.username, &rt, con).unwrap();
157    Ok(rt)
158}
159
160async fn rpc_logout(Json(rtdata): Json<RtData>) {
161    let con = &mut db::con().unwrap();
162    user::del_rt(&rtdata.rt, con).unwrap();
163}
164
165async fn rpc_current(Json(rtdata): Json<RtData>) -> Res<Json<User>> {
166    let con = &mut db::con().unwrap();
167    Ok(Json(get_by_rt(&rtdata.rt, con)?.0))
168}
169
170async fn rpc_access(Json(rtdata): Json<RtData>) -> Res<String> {
171    let rt = rtdata.rt;
172    let claims = verify_rt(&rt).unwrap();
173    let con = &mut db::con().unwrap();
174    let user = get_by_id(claims.user_id, con).unwrap();
175    if user.rt != Some(rt) {
176        return err::res_msg("no such refresh token for user");
177    }
178    // we don't store access tokens since they intended to be short-lived
179    Ok(new_at(claims.user_id).unwrap())
180}
181
182async fn rpc_get_user_changes(
183    headers: HeaderMap,
184    Json(get_changes): Json<GetChanges>,
185) -> Res<Json<Vec<UserChange>>> {
186    verify_domain_secret_from_headers(headers)?;
187    let con = &mut db::con().unwrap();
188    let changes = user_change::get_many(get_changes.from, con).unwrap();
189    Ok(Json(changes))
190}
191
192fn verify_domain_secret_from_headers(headers: HeaderMap) -> Res<()> {
193    match headers.get("domain_secret") {
194        Some(secret) => {
195            if secret.to_str().unwrap() != &APPRC.domain.secret {
196                return err::res_msg("invalid secret");
197            }
198        }
199        None => return err::res_msg("missing server api token"),
200    }
201    Ok(())
202}
203
204pub fn get_router() -> Router {
205    Router::new()
206        .route("/rpc/login", post(rpc_login))
207        .route("/rpc/logout", post(rpc_logout))
208        .route("/rpc/current", post(rpc_current))
209        .route("/rpc/access", post(rpc_access))
210        // domain-only
211        .route("/rpc/server/reg", post(rpc_reg))
212        .route("/rpc/server/dereg", post(rpc_dereg))
213        .route("/rpc/server/get_user_changes", post(rpc_get_user_changes))
214        .layer(middleware::from_fn(err_middleware))
215}