1use actix_identity::Identity;
7use actix_web::http::header;
8use actix_web::{web, HttpResponse, Responder};
9use db_core::errors::DBError;
10use serde::{Deserialize, Serialize};
11
12use super::mcaptcha::get_random;
13use crate::errors::*;
14use crate::AppData;
15
16pub mod routes {
17 use actix_auth_middleware::GetLoginRoute;
18
19 pub struct Auth {
20 pub logout: &'static str,
21 pub login: &'static str,
22 pub register: &'static str,
23 }
24
25 impl Auth {
26 pub const fn new() -> Auth {
27 let login = "/api/v1/signin";
28 let logout = "/logout";
29 let register = "/api/v1/signup";
30 Auth {
31 logout,
32 login,
33 register,
34 }
35 }
36 }
37
38 impl GetLoginRoute for Auth {
39 fn get_login_route(&self, src: Option<&str>) -> String {
40 if let Some(redirect_to) = src {
41 format!(
42 "{}?redirect_to={}",
43 self.login,
44 urlencoding::encode(redirect_to)
45 )
46 } else {
47 self.login.to_string()
48 }
49 }
50 }
51}
52
53pub mod runners {
54 use super::*;
55
56 #[derive(Clone, Debug, Deserialize, Serialize)]
57 pub struct Register {
58 pub username: String,
59 pub password: String,
60 pub confirm_password: String,
61 pub email: Option<String>,
62 }
63
64 #[derive(Clone, Debug, Deserialize, Serialize)]
65 pub struct Login {
66 pub login: String,
69 pub password: String,
70 }
71
72 #[derive(Clone, Debug, Deserialize, Serialize)]
73 pub struct Password {
74 pub password: String,
75 }
76
77 pub async fn login_runner(payload: Login, data: &AppData) -> ServiceResult<String> {
79 use argon2_creds::Config;
80
81 let verify = |stored: &str, received: &str| {
82 if Config::verify(stored, received)? {
83 Ok(())
84 } else {
85 Err(ServiceError::WrongPassword)
86 }
87 };
88
89 let s = if payload.login.contains('@') {
90 data.db
91 .get_password(&db_core::Login::Email(&payload.login))
92 .await?
93 } else {
94 let username = data.creds.username(&payload.login)?;
95 data.db
96 .get_password(&db_core::Login::Username(&username))
97 .await?
98 };
99
100 verify(&s.hash, &payload.password)?;
101 Ok(s.username)
102 }
103 pub async fn register_runner(
104 payload: &Register,
105 data: &AppData,
106 ) -> ServiceResult<()> {
107 if !data.settings.allow_registration {
108 return Err(ServiceError::ClosedForRegistration);
109 }
110
111 if payload.password != payload.confirm_password {
112 return Err(ServiceError::PasswordsDontMatch);
113 }
114 let username = data.creds.username(&payload.username)?;
115 let hash = data.creds.password(&payload.password)?;
116
117 if let Some(email) = &payload.email {
118 data.creds.email(email)?;
119 }
120
121 let mut secret;
122
123 loop {
124 secret = get_random(32);
125
126 let p = db_core::Register {
127 username: &username,
128 hash: &hash,
129 email: payload.email.as_deref(),
130 secret: &secret,
131 };
132
133 match data.db.register(&p).await {
134 Ok(_) => break,
135 Err(DBError::SecretTaken) => continue,
136 Err(e) => return Err(e.into()),
137 }
138 }
139
140 Ok(())
141 }
142}
143
144pub fn services(cfg: &mut web::ServiceConfig) {
145 cfg.service(register);
146 cfg.service(login);
147 cfg.service(signout);
148}
149#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")]
150async fn register(
151 payload: web::Json<runners::Register>,
152 data: AppData,
153) -> ServiceResult<impl Responder> {
154 runners::register_runner(&payload, &data).await?;
155 Ok(HttpResponse::Ok())
156}
157
158#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login")]
159async fn login(
160 id: Identity,
161 payload: web::Json<runners::Login>,
162 query: web::Query<super::RedirectQuery>,
163 data: AppData,
164) -> ServiceResult<impl Responder> {
165 let username = runners::login_runner(payload.into_inner(), &data).await?;
166 id.remember(username);
167 let query = query.into_inner();
170 if let Some(redirect_to) = query.redirect_to {
171 Ok(HttpResponse::Found()
172 .append_header((header::LOCATION, redirect_to))
173 .finish())
174 } else {
175 Ok(HttpResponse::Ok().finish())
176 }
177}
178
179#[my_codegen::get(
180 path = "crate::V1_API_ROUTES.auth.logout",
181 wrap = "crate::api::v1::get_middleware()"
182)]
183async fn signout(id: Identity) -> impl Responder {
184 if id.identity().is_some() {
185 id.forget();
186 }
187 HttpResponse::Found()
188 .append_header((header::LOCATION, crate::PAGES.auth.login))
189 .finish()
190}