2023-08-01 16:29:03 +02:00
|
|
|
use std::ops::Add;
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
2023-08-14 12:30:32 +02:00
|
|
|
use actix_jwt_session::SessionStorage;
|
2023-08-15 13:36:35 +02:00
|
|
|
pub use actix_jwt_session::{Error, RedisMiddlewareFactory};
|
2023-08-04 16:32:10 +02:00
|
|
|
use actix_web::web::{Data, Form, ServiceConfig};
|
2023-08-14 17:21:18 +02:00
|
|
|
use actix_web::{get, post, HttpRequest, HttpResponse};
|
2023-08-11 15:25:26 +02:00
|
|
|
use askama_actix::Template;
|
2023-08-03 16:16:46 +02:00
|
|
|
use autometrics::autometrics;
|
2023-08-05 22:20:23 +02:00
|
|
|
use garde::Validate;
|
2023-08-01 16:29:03 +02:00
|
|
|
use jsonwebtoken::*;
|
2023-08-16 16:53:27 +02:00
|
|
|
use oswilno_view::{Blank, Errors, Lang, Layout, Main, MainOpts, TranslationStorage};
|
2023-08-01 16:29:03 +02:00
|
|
|
use ring::rand::SystemRandom;
|
|
|
|
use ring::signature::{Ed25519KeyPair, KeyPair};
|
2023-08-02 12:45:29 +02:00
|
|
|
use sea_orm::DatabaseConnection;
|
2023-08-01 16:29:03 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use time::OffsetDateTime;
|
2023-08-02 12:45:29 +02:00
|
|
|
|
|
|
|
mod hashing;
|
2023-08-01 16:29:03 +02:00
|
|
|
|
2023-08-04 22:39:04 +02:00
|
|
|
pub use oswilno_view::filters;
|
|
|
|
|
2023-08-15 13:36:35 +02:00
|
|
|
pub type Authenticated = actix_jwt_session::Authenticated<Claims>;
|
|
|
|
pub type MaybeAuthenticated = actix_jwt_session::MaybeAuthenticated<Claims>;
|
2023-08-01 22:06:04 +02:00
|
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
2023-08-14 12:30:32 +02:00
|
|
|
pub struct JWTTtl(std::time::Duration);
|
2023-08-01 22:06:04 +02:00
|
|
|
|
2023-08-06 22:14:03 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
2023-08-11 15:25:26 +02:00
|
|
|
#[serde(rename_all = "snake_case")]
|
2023-08-15 13:36:35 +02:00
|
|
|
pub enum Audience {
|
2023-08-02 12:45:29 +02:00
|
|
|
Web,
|
|
|
|
}
|
|
|
|
|
2023-08-06 22:14:03 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
2023-08-11 15:25:26 +02:00
|
|
|
#[serde(rename_all = "snake_case")]
|
2023-08-01 22:06:04 +02:00
|
|
|
pub struct Claims {
|
2023-08-02 12:45:29 +02:00
|
|
|
#[serde(rename = "exp")]
|
2023-08-15 13:36:35 +02:00
|
|
|
pub expires_at: usize,
|
2023-08-02 12:45:29 +02:00
|
|
|
#[serde(rename = "iat")]
|
2023-08-15 13:36:35 +02:00
|
|
|
pub issues_at: usize,
|
2023-08-14 17:21:18 +02:00
|
|
|
/// Account login
|
2023-08-02 12:45:29 +02:00
|
|
|
#[serde(rename = "sub")]
|
2023-08-15 13:36:35 +02:00
|
|
|
pub subject: String,
|
2023-08-02 12:45:29 +02:00
|
|
|
#[serde(rename = "aud")]
|
2023-08-15 13:36:35 +02:00
|
|
|
pub audience: Audience,
|
2023-08-02 12:45:29 +02:00
|
|
|
#[serde(rename = "jti")]
|
2023-08-15 13:36:35 +02:00
|
|
|
pub jwt_id: uuid::Uuid,
|
2023-08-14 17:21:18 +02:00
|
|
|
#[serde(rename = "aci")]
|
2023-08-15 13:36:35 +02:00
|
|
|
pub account_id: i32,
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
|
|
|
|
2023-08-13 15:31:05 +02:00
|
|
|
impl actix_jwt_session::Claims for Claims {
|
|
|
|
fn jti(&self) -> uuid::Uuid {
|
|
|
|
self.jwt_id
|
|
|
|
}
|
2023-08-16 16:53:27 +02:00
|
|
|
|
|
|
|
fn subject(&self) -> &str {
|
|
|
|
&self.subject
|
|
|
|
}
|
2023-08-13 15:31:05 +02:00
|
|
|
}
|
|
|
|
|
2023-08-11 22:31:02 +02:00
|
|
|
impl Claims {
|
|
|
|
pub fn account_id(&self) -> i32 {
|
2023-08-14 17:21:18 +02:00
|
|
|
self.account_id
|
2023-08-11 22:31:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-01 22:06:04 +02:00
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
pub struct EmptyResponse {}
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
pub struct LoginResponse {
|
|
|
|
bearer_token: String,
|
|
|
|
claims: Claims,
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
|
|
|
|
2023-08-01 22:06:04 +02:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct SessionConfigurator {
|
|
|
|
jwt_ttl: Data<JWTTtl>,
|
2023-08-13 15:31:05 +02:00
|
|
|
factory: RedisMiddlewareFactory<Claims>,
|
2023-08-01 22:06:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SessionConfigurator {
|
|
|
|
pub fn app_data(self, config: &mut ServiceConfig) {
|
|
|
|
config
|
|
|
|
.app_data(self.jwt_ttl)
|
|
|
|
.service(login)
|
2023-08-03 16:16:46 +02:00
|
|
|
.service(login_view)
|
2023-08-01 22:06:04 +02:00
|
|
|
.service(logout)
|
2023-08-04 22:39:04 +02:00
|
|
|
.service(session_info)
|
|
|
|
.service(register)
|
2023-08-14 17:21:18 +02:00
|
|
|
.service(register_view);
|
2023-08-01 22:06:04 +02:00
|
|
|
}
|
|
|
|
|
2023-08-13 15:31:05 +02:00
|
|
|
pub fn factory(&self) -> RedisMiddlewareFactory<Claims> {
|
2023-08-02 08:56:53 +02:00
|
|
|
self.factory.clone()
|
|
|
|
}
|
2023-08-01 22:06:04 +02:00
|
|
|
|
2023-08-02 16:37:03 +02:00
|
|
|
pub fn translations(&self, l10n: &mut oswilno_view::TranslationStorage) {
|
2023-08-03 16:16:46 +02:00
|
|
|
l10n
|
|
|
|
// English
|
2023-08-04 22:39:04 +02:00
|
|
|
.with_lang(oswilno_view::Lang::En)
|
2023-08-02 16:37:03 +02:00
|
|
|
.add("Sign in", "Sign in")
|
|
|
|
.add("Sign up", "Sign up")
|
2023-08-03 16:16:46 +02:00
|
|
|
.add("Bad credentials", "Bad credentials")
|
2023-08-02 16:37:03 +02:00
|
|
|
.done()
|
2023-08-03 16:16:46 +02:00
|
|
|
// Polish
|
2023-08-04 22:39:04 +02:00
|
|
|
.with_lang(oswilno_view::Lang::Pl)
|
2023-08-02 16:37:03 +02:00
|
|
|
.add("Sign in", "Logowanie")
|
|
|
|
.add("Sign up", "Rejestracja")
|
2023-08-03 16:16:46 +02:00
|
|
|
.add("Bad credentials", "Złe dane uwierzytelniające")
|
2023-08-05 22:20:23 +02:00
|
|
|
.add("is taken", "jest zajęty")
|
|
|
|
.add("is not strong enough", "jest za słabe")
|
|
|
|
.add(
|
|
|
|
"length is lower than 8",
|
|
|
|
"długość jest mniejsza niż 8 znaków",
|
|
|
|
)
|
2023-08-04 22:39:04 +02:00
|
|
|
.add(
|
|
|
|
"Login or email already taken",
|
|
|
|
"Login lub adres e-mail jest zajęty",
|
|
|
|
)
|
2023-08-05 22:20:23 +02:00
|
|
|
.add("Password", "Hasło")
|
2023-08-04 22:39:04 +02:00
|
|
|
.add("Submit", "Wyślij")
|
2023-08-02 16:37:03 +02:00
|
|
|
.done();
|
|
|
|
}
|
|
|
|
|
2023-08-09 15:42:29 +02:00
|
|
|
pub fn new(redis: redis_async_pool::RedisPool) -> Self {
|
2023-08-14 12:30:32 +02:00
|
|
|
let jwt_ttl = JWTTtl(std::time::Duration::from_secs(31 * 60 * 60));
|
2023-08-01 22:06:04 +02:00
|
|
|
let jwt_signing_keys = JwtSigningKeys::generate().unwrap();
|
2023-08-13 15:31:05 +02:00
|
|
|
let auth_middleware_factory = RedisMiddlewareFactory::<Claims>::new(
|
2023-08-14 12:30:32 +02:00
|
|
|
Arc::new(jwt_signing_keys.encoding_key),
|
2023-08-13 15:31:05 +02:00
|
|
|
Arc::new(jwt_signing_keys.decoding_key),
|
2023-08-14 12:30:32 +02:00
|
|
|
Algorithm::EdDSA,
|
|
|
|
redis,
|
2023-08-13 15:31:05 +02:00
|
|
|
);
|
2023-08-01 22:06:04 +02:00
|
|
|
|
|
|
|
Self {
|
|
|
|
jwt_ttl: Data::new(jwt_ttl.clone()),
|
|
|
|
factory: auth_middleware_factory,
|
|
|
|
}
|
|
|
|
}
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
|
|
|
|
2023-08-03 16:16:46 +02:00
|
|
|
#[derive(Template)]
|
|
|
|
#[template(path = "./sign-in/partial.html")]
|
|
|
|
struct SignInPartialTemplate {
|
|
|
|
form: SignInPayload,
|
2023-08-04 22:39:04 +02:00
|
|
|
lang: Lang,
|
|
|
|
t: Arc<TranslationStorage>,
|
2023-08-09 15:42:29 +02:00
|
|
|
errors: Errors,
|
2023-08-03 16:16:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Default)]
|
2023-08-01 22:38:56 +02:00
|
|
|
pub struct SignInPayload {
|
|
|
|
login: String,
|
|
|
|
password: String,
|
2023-08-03 16:16:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/login")]
|
2023-08-14 17:21:18 +02:00
|
|
|
async fn login_view(req: HttpRequest, t: Data<TranslationStorage>) -> HttpResponse {
|
|
|
|
HttpResponse::Ok().body(
|
|
|
|
if oswilno_view::is_partial(&req) {
|
2023-08-14 22:23:18 +02:00
|
|
|
Main {
|
|
|
|
body: SignInPartialTemplate {
|
2023-08-14 17:21:18 +02:00
|
|
|
form: SignInPayload::default(),
|
|
|
|
lang: Lang::Pl,
|
|
|
|
t: t.into_inner(),
|
|
|
|
errors: Errors::default(),
|
|
|
|
},
|
2023-08-15 12:33:53 +02:00
|
|
|
opts: MainOpts {
|
2023-08-14 22:23:18 +02:00
|
|
|
show: true,
|
|
|
|
search: None,
|
|
|
|
session: None,
|
|
|
|
},
|
2023-08-16 08:48:39 +02:00
|
|
|
title: Blank,
|
2023-08-14 22:23:18 +02:00
|
|
|
}
|
|
|
|
.render()
|
|
|
|
} else {
|
|
|
|
Layout {
|
|
|
|
main: Main {
|
|
|
|
body: SignInPartialTemplate {
|
|
|
|
form: SignInPayload::default(),
|
|
|
|
lang: Lang::Pl,
|
|
|
|
t: t.into_inner(),
|
|
|
|
errors: Errors::default(),
|
|
|
|
},
|
2023-08-15 12:33:53 +02:00
|
|
|
opts: MainOpts {
|
2023-08-14 22:23:18 +02:00
|
|
|
show: true,
|
|
|
|
..Default::default()
|
|
|
|
},
|
2023-08-16 08:48:39 +02:00
|
|
|
title: Blank,
|
2023-08-14 22:23:18 +02:00
|
|
|
},
|
2023-08-14 17:21:18 +02:00
|
|
|
}
|
|
|
|
.render()
|
|
|
|
}
|
|
|
|
.unwrap_or_default(),
|
|
|
|
)
|
2023-08-05 14:48:13 +02:00
|
|
|
}
|
2023-08-01 22:38:56 +02:00
|
|
|
|
2023-08-03 16:16:46 +02:00
|
|
|
#[autometrics]
|
2023-08-01 22:38:56 +02:00
|
|
|
#[post("/login")]
|
2023-08-01 16:29:03 +02:00
|
|
|
async fn login(
|
2023-08-01 22:06:04 +02:00
|
|
|
jwt_ttl: Data<JWTTtl>,
|
2023-08-01 22:38:56 +02:00
|
|
|
db: Data<DatabaseConnection>,
|
2023-08-14 12:30:32 +02:00
|
|
|
redis: Data<SessionStorage<Claims>>,
|
2023-08-03 16:16:46 +02:00
|
|
|
payload: Form<SignInPayload>,
|
|
|
|
t: Data<oswilno_view::TranslationStorage>,
|
2023-08-06 22:14:03 +02:00
|
|
|
lang: Lang,
|
2023-08-01 16:29:03 +02:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2023-08-04 22:39:04 +02:00
|
|
|
let t = t.into_inner();
|
2023-08-09 15:42:29 +02:00
|
|
|
let mut errors = Errors::default();
|
|
|
|
match login_inner(
|
2023-08-11 15:25:26 +02:00
|
|
|
jwt_ttl.into_inner(),
|
2023-08-09 15:42:29 +02:00
|
|
|
payload.into_inner(),
|
|
|
|
db.into_inner(),
|
|
|
|
redis.into_inner(),
|
|
|
|
&mut errors,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
2023-08-15 12:33:53 +02:00
|
|
|
Ok(res) => Ok(res),
|
2023-08-09 15:42:29 +02:00
|
|
|
Err(form) => Ok(HttpResponse::Ok().body(
|
2023-08-11 15:25:26 +02:00
|
|
|
(SignInPartialTemplate {
|
|
|
|
form,
|
|
|
|
lang,
|
|
|
|
t,
|
|
|
|
errors,
|
|
|
|
})
|
2023-08-09 15:42:29 +02:00
|
|
|
.render()
|
|
|
|
.unwrap(),
|
|
|
|
)),
|
2023-08-03 16:16:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn login_inner(
|
2023-08-11 15:25:26 +02:00
|
|
|
jwt_ttl: Arc<JWTTtl>,
|
2023-08-09 15:42:29 +02:00
|
|
|
payload: SignInPayload,
|
|
|
|
db: Arc<DatabaseConnection>,
|
2023-08-14 12:30:32 +02:00
|
|
|
redis: Arc<SessionStorage<Claims>>,
|
2023-08-09 15:42:29 +02:00
|
|
|
errors: &mut Errors,
|
2023-08-15 12:33:53 +02:00
|
|
|
) -> Result<HttpResponse, SignInPayload> {
|
2023-08-01 16:29:03 +02:00
|
|
|
let iat = OffsetDateTime::now_utc().unix_timestamp() as usize;
|
|
|
|
let expires_at = OffsetDateTime::now_utc().add(jwt_ttl.0);
|
|
|
|
let exp = expires_at.unix_timestamp() as usize;
|
|
|
|
|
2023-08-01 22:38:56 +02:00
|
|
|
use sea_orm::*;
|
2023-08-02 08:56:53 +02:00
|
|
|
let account = match oswilno_contract::accounts::Entity::find()
|
|
|
|
.filter(oswilno_contract::accounts::Column::Login.eq(payload.login.as_str()))
|
2023-08-16 08:04:48 +02:00
|
|
|
.filter(oswilno_contract::accounts::Column::Banned.eq(false))
|
|
|
|
// TODO: Add email confirmation
|
|
|
|
//.filter(oswilno_contract::accounts::Column::Confirmed.eq(true))
|
2023-08-02 08:56:53 +02:00
|
|
|
.one(&*db)
|
|
|
|
.await
|
|
|
|
{
|
2023-08-02 12:45:29 +02:00
|
|
|
Ok(Some(a)) => a,
|
|
|
|
Ok(None) => {
|
2023-08-09 15:42:29 +02:00
|
|
|
errors.push_global("Bad credentials");
|
2023-08-03 16:16:46 +02:00
|
|
|
return Err(payload);
|
2023-08-02 12:45:29 +02:00
|
|
|
}
|
2023-08-01 22:38:56 +02:00
|
|
|
Err(e) => {
|
|
|
|
tracing::warn!("Failed to find account: {e}");
|
2023-08-09 15:42:29 +02:00
|
|
|
errors.push_global("Bad credentials");
|
2023-08-03 16:16:46 +02:00
|
|
|
return Err(payload);
|
2023-08-01 22:38:56 +02:00
|
|
|
}
|
|
|
|
};
|
2023-08-15 12:33:53 +02:00
|
|
|
if let Err(e) = hashing::verify(account.pass_hash.as_str(), payload.password.as_str()) {
|
|
|
|
tracing::warn!("Hashing verification failed: {e}");
|
2023-08-09 15:42:29 +02:00
|
|
|
errors.push_global("Bad credentials");
|
2023-08-03 16:16:46 +02:00
|
|
|
return Err(payload);
|
2023-08-02 12:45:29 +02:00
|
|
|
}
|
2023-08-01 22:38:56 +02:00
|
|
|
|
2023-08-02 12:45:29 +02:00
|
|
|
let jwt_claims = Claims {
|
|
|
|
issues_at: iat,
|
2023-08-14 17:21:18 +02:00
|
|
|
subject: account.login.clone(),
|
2023-08-02 12:45:29 +02:00
|
|
|
expires_at: exp,
|
|
|
|
audience: Audience::Web,
|
|
|
|
jwt_id: uuid::Uuid::new_v4(),
|
2023-08-14 17:21:18 +02:00
|
|
|
account_id: account.id,
|
2023-08-02 12:45:29 +02:00
|
|
|
};
|
2023-08-14 12:30:32 +02:00
|
|
|
let jwt_token = match redis.store(jwt_claims.clone(), jwt_ttl.0).await {
|
|
|
|
Err(e) => {
|
|
|
|
tracing::warn!("Failed to set sign-in claims in redis: {e}");
|
2023-08-09 15:42:29 +02:00
|
|
|
errors.push_global("Failed to sign in. Please try later");
|
|
|
|
return Err(payload);
|
2023-08-14 12:30:32 +02:00
|
|
|
}
|
|
|
|
Ok(jwt_token) => jwt_token,
|
|
|
|
};
|
|
|
|
let bearer_token = match jwt_token.encode() {
|
|
|
|
Ok(token) => token,
|
|
|
|
Err(e) => {
|
2023-08-14 17:21:18 +02:00
|
|
|
tracing::warn!("Failed to encode claims: {e}");
|
2023-08-09 15:42:29 +02:00
|
|
|
errors.push_global("Failed to sign in. Please try later");
|
|
|
|
return Err(payload);
|
|
|
|
}
|
2023-08-14 12:30:32 +02:00
|
|
|
};
|
2023-08-16 16:53:27 +02:00
|
|
|
|
|
|
|
let cookie = actix_web::cookie::Cookie::build(actix_jwt_session::HEADER_NAME, &bearer_token).http_only(true).finish();
|
2023-08-15 12:33:53 +02:00
|
|
|
Ok(HttpResponse::Ok()
|
|
|
|
.append_header((
|
|
|
|
actix_jwt_session::HEADER_NAME,
|
|
|
|
format!("Bearer {bearer_token}").as_str(),
|
|
|
|
))
|
2023-08-16 16:53:27 +02:00
|
|
|
.cookie(cookie)
|
2023-08-15 12:33:53 +02:00
|
|
|
.body(""))
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
|
|
|
|
2023-08-03 16:16:46 +02:00
|
|
|
#[autometrics]
|
2023-08-01 16:29:03 +02:00
|
|
|
#[get("/session")]
|
2023-08-15 13:36:35 +02:00
|
|
|
async fn session_info(authenticated: Authenticated) -> Result<HttpResponse, Error> {
|
2023-08-13 15:31:05 +02:00
|
|
|
Ok(HttpResponse::Ok().json(&*authenticated))
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
|
|
|
|
2023-08-03 16:16:46 +02:00
|
|
|
#[autometrics]
|
2023-08-01 16:29:03 +02:00
|
|
|
#[get("/logout")]
|
|
|
|
async fn logout(
|
2023-08-15 13:36:35 +02:00
|
|
|
authenticated: Authenticated,
|
2023-08-14 17:21:18 +02:00
|
|
|
redis: Data<SessionStorage<Claims>>,
|
2023-08-01 16:29:03 +02:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2023-08-14 17:21:18 +02:00
|
|
|
let jwt_id = authenticated.jwt_id;
|
|
|
|
if let Err(_e) = redis.erase(jwt_id).await {};
|
|
|
|
Ok(HttpResponse::SeeOther()
|
|
|
|
.append_header(("Location", "/"))
|
|
|
|
.body(""))
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
2023-08-01 22:06:04 +02:00
|
|
|
|
2023-08-05 22:20:23 +02:00
|
|
|
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, garde::Validate)]
|
|
|
|
#[garde(context(RegisterContext))]
|
2023-08-02 08:56:53 +02:00
|
|
|
struct AccountInfo {
|
2023-08-05 22:20:23 +02:00
|
|
|
#[garde(length(min = 4, max = 30), custom(is_login_free))]
|
|
|
|
#[serde(rename = "login")]
|
|
|
|
input_login: String,
|
|
|
|
#[garde(email, custom(is_email_free))]
|
2023-08-04 22:39:04 +02:00
|
|
|
email: String,
|
2023-08-05 22:20:23 +02:00
|
|
|
#[garde(length(min = 8, max = 50), custom(is_strong_password))]
|
2023-08-02 08:56:53 +02:00
|
|
|
password: String,
|
2023-08-15 13:36:35 +02:00
|
|
|
#[garde(
|
|
|
|
length(min = 8, max = 50),
|
|
|
|
custom(is_strong_password),
|
|
|
|
custom(check_pass_differ)
|
|
|
|
)]
|
2023-08-15 12:33:53 +02:00
|
|
|
password_confirmation: String,
|
2023-08-04 22:39:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/register")]
|
2023-08-14 17:21:18 +02:00
|
|
|
async fn register_view(req: HttpRequest, t: Data<TranslationStorage>) -> HttpResponse {
|
|
|
|
HttpResponse::Ok().body(
|
|
|
|
if oswilno_view::is_partial(&req) {
|
2023-08-14 22:23:18 +02:00
|
|
|
Main {
|
|
|
|
body: RegisterPartialTemplate {
|
2023-08-14 17:21:18 +02:00
|
|
|
form: AccountInfo::default(),
|
|
|
|
t: t.into_inner(),
|
|
|
|
lang: Lang::Pl,
|
|
|
|
errors: oswilno_view::Errors::default(),
|
|
|
|
},
|
2023-08-16 08:48:39 +02:00
|
|
|
title: Blank,
|
2023-08-15 12:33:53 +02:00
|
|
|
opts: MainOpts {
|
2023-08-14 22:23:18 +02:00
|
|
|
show: true,
|
|
|
|
search: None,
|
|
|
|
session: None,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
.render()
|
|
|
|
} else {
|
|
|
|
Layout {
|
|
|
|
main: Main {
|
|
|
|
body: RegisterPartialTemplate {
|
|
|
|
form: AccountInfo::default(),
|
|
|
|
t: t.into_inner(),
|
|
|
|
lang: Lang::Pl,
|
|
|
|
errors: oswilno_view::Errors::default(),
|
|
|
|
},
|
2023-08-16 08:48:39 +02:00
|
|
|
title: Blank,
|
2023-08-15 12:33:53 +02:00
|
|
|
opts: MainOpts::default(),
|
2023-08-14 22:23:18 +02:00
|
|
|
},
|
2023-08-14 17:21:18 +02:00
|
|
|
}
|
|
|
|
.render()
|
|
|
|
}
|
|
|
|
.unwrap_or_default(),
|
|
|
|
)
|
2023-08-05 14:48:13 +02:00
|
|
|
}
|
2023-08-04 22:39:04 +02:00
|
|
|
|
|
|
|
#[derive(askama_actix::Template)]
|
|
|
|
#[template(path = "./register/partial.html")]
|
|
|
|
struct RegisterPartialTemplate {
|
|
|
|
form: AccountInfo,
|
|
|
|
t: Arc<TranslationStorage>,
|
|
|
|
lang: Lang,
|
2023-08-05 22:20:23 +02:00
|
|
|
errors: oswilno_view::Errors,
|
2023-08-02 08:56:53 +02:00
|
|
|
}
|
|
|
|
|
2023-08-03 16:16:46 +02:00
|
|
|
#[autometrics]
|
2023-08-02 08:56:53 +02:00
|
|
|
#[post("/register")]
|
|
|
|
async fn register(
|
2023-08-15 12:33:53 +02:00
|
|
|
req: HttpRequest,
|
2023-08-02 08:56:53 +02:00
|
|
|
db: Data<DatabaseConnection>,
|
2023-08-04 16:32:10 +02:00
|
|
|
payload: Form<AccountInfo>,
|
2023-08-04 22:39:04 +02:00
|
|
|
t: Data<oswilno_view::TranslationStorage>,
|
2023-08-06 22:14:03 +02:00
|
|
|
lang: Lang,
|
2023-08-02 08:56:53 +02:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2023-08-04 22:39:04 +02:00
|
|
|
let t = t.into_inner();
|
2023-08-05 22:20:23 +02:00
|
|
|
let mut errors = oswilno_view::Errors::default();
|
2023-08-04 22:39:04 +02:00
|
|
|
Ok(
|
2023-08-15 12:33:53 +02:00
|
|
|
match register_internal(req, db.into_inner(), payload.into_inner(), &mut errors).await {
|
2023-08-04 22:39:04 +02:00
|
|
|
Ok(res) => res,
|
|
|
|
Err(p) => HttpResponse::BadRequest().body(
|
|
|
|
RegisterPartialTemplate {
|
|
|
|
form: p,
|
|
|
|
t,
|
2023-08-06 22:14:03 +02:00
|
|
|
lang,
|
2023-08-05 22:20:23 +02:00
|
|
|
errors,
|
2023-08-04 22:39:04 +02:00
|
|
|
}
|
|
|
|
.render()
|
|
|
|
.unwrap(),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-05 22:20:23 +02:00
|
|
|
struct RegisterContext {
|
|
|
|
login_taken: bool,
|
|
|
|
email_taken: bool,
|
2023-08-15 12:33:53 +02:00
|
|
|
pass_differ: bool,
|
2023-08-05 22:20:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn is_email_free(_value: &str, context: &RegisterContext) -> garde::Result {
|
|
|
|
if context.email_taken {
|
|
|
|
return Err(garde::Error::new("is taken"));
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
fn is_login_free(_value: &str, context: &RegisterContext) -> garde::Result {
|
|
|
|
if context.login_taken {
|
|
|
|
return Err(garde::Error::new("is taken"));
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
static WEAK_PASS: &str = "is not strong enough";
|
2023-08-15 12:33:53 +02:00
|
|
|
fn check_pass_differ(_v: &str, ctx: &RegisterContext) -> garde::Result {
|
|
|
|
if ctx.pass_differ {
|
|
|
|
Err(garde::Error::new(DIFFER_PASS))
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static DIFFER_PASS: &str = "passwords differ";
|
2023-08-05 22:20:23 +02:00
|
|
|
fn is_strong_password(value: &str, _context: &RegisterContext) -> garde::Result {
|
|
|
|
if !(8..50).contains(&value.len()) {
|
|
|
|
return Err(garde::Error::new(WEAK_PASS));
|
|
|
|
}
|
|
|
|
let mut num = false;
|
|
|
|
let mut low = false;
|
|
|
|
let mut up = false;
|
|
|
|
let mut spec = false;
|
|
|
|
for c in value.chars() {
|
|
|
|
if num && low && up && spec {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
num = num || c.is_numeric();
|
|
|
|
low = low || c.is_lowercase();
|
|
|
|
up = up || c.is_uppercase();
|
|
|
|
spec = spec || !c.is_alphanumeric();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Err(garde::Error::new(WEAK_PASS));
|
|
|
|
}
|
|
|
|
|
2023-08-04 22:39:04 +02:00
|
|
|
async fn register_internal(
|
2023-08-15 12:33:53 +02:00
|
|
|
_req: HttpRequest,
|
2023-08-04 22:39:04 +02:00
|
|
|
db: Arc<DatabaseConnection>,
|
2023-08-05 22:20:23 +02:00
|
|
|
p: AccountInfo,
|
|
|
|
errors: &mut oswilno_view::Errors,
|
2023-08-04 22:39:04 +02:00
|
|
|
) -> Result<HttpResponse, AccountInfo> {
|
2023-08-02 08:56:53 +02:00
|
|
|
use oswilno_contract::accounts::*;
|
2023-08-02 12:45:29 +02:00
|
|
|
use sea_orm::*;
|
2023-08-02 08:56:53 +02:00
|
|
|
|
2023-08-05 22:20:23 +02:00
|
|
|
let query_result = db
|
|
|
|
.query_one(sea_orm::Statement::from_sql_and_values(
|
|
|
|
sea_orm::DbBackend::Postgres,
|
|
|
|
"select login = $1 as login_taken, email = $2 as email_taken from accounts",
|
|
|
|
[p.input_login.clone().into(), p.email.clone().into()],
|
|
|
|
))
|
|
|
|
.await
|
|
|
|
.map_err(|e| {
|
|
|
|
tracing::error!("{e}");
|
|
|
|
errors.push_global("Something went wrong");
|
|
|
|
p.clone()
|
|
|
|
})?;
|
|
|
|
let (login_taken, email_taken) = if let Some(query_result) = query_result {
|
|
|
|
let Ok((login_taken, email_taken)): Result<(bool,bool), _> = query_result.try_get_many("", &["login_taken".into(), "email_taken".into()]) else {
|
|
|
|
tracing::warn!("Failed to fetch fields from query result while checking if account info exists in db");
|
|
|
|
errors.push_global("Something went wrong");
|
|
|
|
return Err(p);
|
|
|
|
};
|
|
|
|
(login_taken, email_taken)
|
|
|
|
} else {
|
|
|
|
(false, false)
|
|
|
|
};
|
|
|
|
|
|
|
|
if let Err(e) = p.validate(&RegisterContext {
|
|
|
|
login_taken,
|
|
|
|
email_taken,
|
2023-08-15 12:33:53 +02:00
|
|
|
pass_differ: p.password != p.password_confirmation,
|
2023-08-05 22:20:23 +02:00
|
|
|
}) {
|
|
|
|
errors.consume_garde(e);
|
2023-08-06 22:14:03 +02:00
|
|
|
return Err(p);
|
2023-08-05 22:20:23 +02:00
|
|
|
}
|
|
|
|
tracing::warn!("{errors:#?}");
|
|
|
|
|
2023-08-02 12:45:29 +02:00
|
|
|
let pass = match hashing::encrypt(p.password.as_str()) {
|
|
|
|
Ok(p) => p,
|
|
|
|
Err(e) => {
|
|
|
|
tracing::warn!("{e}");
|
|
|
|
return Ok(HttpResponse::InternalServerError().body(""));
|
|
|
|
}
|
|
|
|
};
|
2023-08-06 22:14:03 +02:00
|
|
|
let model = (ActiveModel {
|
2023-08-02 12:45:29 +02:00
|
|
|
id: NotSet,
|
2023-08-05 22:20:23 +02:00
|
|
|
login: Set(p.input_login.to_string()),
|
2023-08-05 14:48:13 +02:00
|
|
|
email: Set(p.email.to_string()),
|
2023-08-02 12:45:29 +02:00
|
|
|
pass_hash: Set(pass),
|
|
|
|
..Default::default()
|
|
|
|
})
|
|
|
|
.save(&*db)
|
|
|
|
.await
|
2023-08-06 22:14:03 +02:00
|
|
|
.map_err(|e| {
|
|
|
|
tracing::warn!("{e}");
|
|
|
|
errors.push_global("Login or email already taken");
|
|
|
|
p
|
|
|
|
})?;
|
2023-08-02 12:45:29 +02:00
|
|
|
|
|
|
|
tracing::info!("{model:?}");
|
|
|
|
|
|
|
|
Ok(HttpResponse::SeeOther()
|
2023-08-02 16:37:03 +02:00
|
|
|
.append_header(("Location", "/login"))
|
2023-08-15 12:33:53 +02:00
|
|
|
.append_header(("Accept", "text/html-partial"))
|
|
|
|
.body(""))
|
2023-08-02 08:56:53 +02:00
|
|
|
}
|
|
|
|
|
2023-08-13 15:31:05 +02:00
|
|
|
pub struct JwtSigningKeys {
|
2023-08-01 22:06:04 +02:00
|
|
|
encoding_key: EncodingKey,
|
|
|
|
decoding_key: DecodingKey,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl JwtSigningKeys {
|
|
|
|
fn generate() -> Result<Self, Box<dyn std::error::Error>> {
|
|
|
|
let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?;
|
|
|
|
let keypair = Ed25519KeyPair::from_pkcs8(doc.as_ref())?;
|
|
|
|
let encoding_key = EncodingKey::from_ed_der(doc.as_ref());
|
|
|
|
let decoding_key = DecodingKey::from_ed_der(keypair.public_key().as_ref());
|
|
|
|
Ok(JwtSigningKeys {
|
|
|
|
encoding_key,
|
|
|
|
decoding_key,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|