use crate::prelude2::*;
use actix_session::Session;
use actix_web_flash_messages::{FlashMessage, IncomingFlashMessages};
use base64::{engine::general_purpose, Engine as _};
use captcha::filters::Noise;
use captcha::filters::Wave;
use captcha::Captcha;
use captcha::Geometry;
use rand::Rng;
use serde_json::json;
use crate::commons::validatorr::validation_flatten;
use crate::core::auth0::user_session::TypedSession;
use crate::core::auth0::Agent;
use crate::core::auth0::Language;
use crate::core::auth0::Requestor;
use crate::core::{auth0, user_agent};
use crate::core::csrf::extractor::CsrfGuarded;
use crate::core::csrf::extractor::CsrfToken;
use crate::utils;
#[derive(serde::Deserialize)]
pub struct LoginFormData {
csrf_token: CsrfToken,
username: String,
password: String,
}
impl CsrfGuarded for LoginFormData {
fn csrf_token(&self) -> &CsrfToken {
&self.csrf_token
}
}
fn default_code_length() -> usize {
6
}
#[derive(Debug, validator::Validate, serde::Serialize, serde::Deserialize)]
pub struct SmsCodeSendData {
#[validate(length(min = 32, max = 64))]
id: String,
#[validate(length(min = 11, max = 18))]
phone: String,
#[serde(default = "default_code_length")]
#[validate(range(min = 4, max = 6))]
length: usize,
captcha_code: String,
}
#[derive(Debug, validator::Validate, serde::Serialize, serde::Deserialize)]
pub struct SmsCodeCheckParam {
#[validate(length(min = 32, max = 64))]
id: String,
#[validate(length(min = 11, max = 18))]
phone: String,
#[validate(length(min = 1, max = 10))]
code: String,
}
fn default_captcha_code_length() -> u32 {
6
}
fn default_width() -> u32 {
220
}
fn default_height() -> u32 {
80
}
fn default_type() -> String {
"png".to_string()
}
#[derive(Debug, validator::Validate, serde::Serialize, serde::Deserialize)]
pub struct CaptchaFormData {
#[validate(length(min = 11, max = 18))]
phone: String,
#[serde(default = "default_captcha_code_length")]
#[validate(range(min = 6, max = 8))]
length: u32,
#[serde(default = "default_width")]
#[validate(range(min = 220, max = 300))]
width: u32,
#[serde(default = "default_height")]
#[validate(range(min = 80, max = 300))]
height: u32,
#[serde(default = "default_type")]
typ: String,
}
pub async fn home(
_requestor: web::ReqData<Requestor>,
_lang: web::ReqData<Language>,
request: HttpRequest,
_agent: web::ReqData<Agent>,
) -> impl Responder {
log::info!("_agent, val={}", _agent.0);
log::info!("_agent, val={:?}", user_agent::parse(&_agent.0));
let mut ctx = tera::Context::new();
ctx.insert("greeting", "Hi");
request.render(200, "defaults/index.html", ctx)
}
pub async fn e405(request: HttpRequest) -> impl Responder {
request.json(200, R::failed(405, String::from("method not allowed")))
}
pub async fn not_found(request: HttpRequest) -> impl Responder {
let _path = request.path();
let accept = crate::utils::get_header_value(&request, "Accept");
let upgrade = crate::utils::get_header_value(&request, "upgrade");
if accept.contains("json") || upgrade == "websocket" {
return request.json(404, R::failed(404, "404 Not Found".to_string()));
}
let ctx = tera::Context::new();
request.render(200, "defaults/404.html", ctx)
}
pub async fn login_form(
csrf_token: CsrfToken,
flash_messages: IncomingFlashMessages,
request: HttpRequest,
) -> impl Responder {
let _flash_html = crate::core::flash_messages::get_flash_message_html(&flash_messages);
let mut ctx = tera::Context::new();
ctx.insert("__flash_message", &_flash_html);
ctx.insert("__csrf_token", csrf_token.get());
request.render(200, "defaults/login.html", ctx)
}
pub async fn signup_form(
csrf_token: CsrfToken,
flash_messages: IncomingFlashMessages,
request: HttpRequest,
) -> impl Responder {
let _flash_html = crate::core::flash_messages::get_flash_message_html(&flash_messages);
let mut ctx = tera::Context::new();
ctx.insert("__flash_message", &_flash_html);
ctx.insert("__csrf_token", csrf_token.get());
request.render(200, "defaults/signup.html", ctx)
}
pub async fn logout(requestor: web::ReqData<Requestor>, session: Session) -> Result<HttpResponse> {
let requestor = requestor.into_inner();
if requestor.get_user().is_some() {
let session = TypedSession(session);
if session
.get_user_id()
.map_err(utils::e500)
.map_err(|e| Error::throw("", Some(e)))?
.is_none()
{
return Ok(utils::see_other("/login"));
}
session.log_out();
FlashMessage::info("You have successfully logged out.").send();
}
Ok(utils::see_other("/login"))
}
pub async fn login_post_flash(
form: actix_web::web::Form<LoginFormData>,
context: web::Data<AppContext>,
session: TypedSession,
_lang: web::ReqData<Language>,
_agent: web::ReqData<Agent>,
request: HttpRequest,
) -> Result<HttpResponse> {
let credentials = auth0::Credentials {
username: form.username.clone(),
password: form.password.clone(),
};
let request_with = request
.headers()
.get("X-Requested-With")
.map(|val| val.to_str().unwrap_or_default())
.unwrap_or("");
log::info!("csrf: value={:?}", (form.csrf_token).clone().into_inner());
let user_id = auth0::validate_credentials(&credentials, context.mysql()).await;
if let Some(user_id) = user_id {
session.renew();
session
.insert_user_id(user_id)
.map_err(|e| Error::UnexpectedError(e.into()))?;
if request_with == "fetch" {
return request.json(200, R::ok("/"));
}
return Ok(utils::see_other("/"));
}
log::warn!(
"Login Failed: username={}, request_with={}",
credentials.username,
request_with
);
if request_with == "fetch" {
request.json(200, R::failed(401, "login failed"))
} else {
FlashMessage::error(format!("Login Failed: username={}", credentials.username)).send();
Ok(utils::see_other("/login"))
}
}
pub async fn access_token(
form: actix_web::web::Json<LoginFormData>,
app_state: web::Data<AppContext>,
lang: web::ReqData<Language>,
agent: web::ReqData<Agent>,
request: HttpRequest,
) -> impl Responder {
use crate::core::auth0::jwt_token::JwtToken;
let credentials = auth0::Credentials {
username: form.username.clone(),
password: form.password.clone(),
};
let user_id = auth0::validate_credentials(&credentials, app_state.mysql()).await;
log::info!(
"username={}, lang={}, Agent={}",
credentials.username,
lang.into_inner(),
agent.into_inner()
);
if let Some(user_id) = user_id {
let user_id = user_id.0;
return request.json(
200,
R::ok(JwtToken::generate_token(&user_id, &credentials.username)?),
);
}
request.json(200, R::failed(401, "Unauthorized".to_string()))
}
pub async fn send_sms_code(
form: web::Json<SmsCodeSendData>,
app_state: web::Data<AppContext>,
_lang: web::ReqData<Language>,
_agent: web::ReqData<Agent>,
request: HttpRequest,
) -> impl Responder {
if let Some(err) = validation_flatten(&form.0) {
return request.json(200, R::invalid(err));
}
use std::time::{SystemTime, UNIX_EPOCH};
let mut rng = rand::rng();
let phone = &form.phone;
let _captcha_code = &form.captcha_code;
let digit_range = rand::distr::Uniform::try_from(0..9).unwrap();
let code = (0..form.length)
.map(|_| rng.sample(digit_range).to_string())
.collect::<String>();
let expired_at = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(duration) => duration.as_secs() + 60, Err(_) => 0,
};
let mut masked_phone = String::new();
if phone.len() >= 11 {
masked_phone.push_str(&phone[..8]);
masked_phone.push_str("****");
masked_phone.push_str(&phone[12..]);
}
let _key = "_".to_owned() + phone + "_" + &form.id;
app_state
.rudis()
.set(&_key, &code, 90)
.await
.map_err(|e| Error::throw("缓存短信验证码异常", Some(e)))?;
log::debug!(
"send_sms_code: id={}, phone={}, code={}",
form.id,
masked_phone,
code
);
let obj = json!({
"id": form.id.to_owned(),
"phone": masked_phone,
"expired_at": expired_at
});
request.json(200, R::ok(obj))
}
pub async fn check_sms_code(
form: web::Json<SmsCodeCheckParam>,
app_state: web::Data<AppContext>,
_lang: web::ReqData<Language>,
_agent: web::ReqData<Agent>,
request: HttpRequest,
) -> impl Responder {
if let Some(err) = validation_flatten(&form.0) {
return request.json(200, R::invalid(err));
}
let phone = &form.phone;
let code = &form.code;
let key = "_".to_owned() + phone + "_" + &form.id;
let cached_code = app_state
.rudis()
.get(&key)
.await
.map_err(|e| Error::throw("验证短信验证码异常", Some(e)))?;
match cached_code {
Some(c) => {
if c == *code {
request.json(200, R::ok(true))
} else {
request.json(200, R::ok(false))
}
}
None => request.json(200, R::ok(false)),
}
}
pub async fn captcha_code(query_params: web::Query<CaptchaFormData>) -> impl Responder {
if let Some(err) = validation_flatten(&query_params.0) {
return HttpResponse::Ok()
.content_type("application/json")
.body(R::invalid(err).to_string());
}
let mut c = Captcha::new();
c.add_chars(query_params.length)
.apply_filter(Noise::new(0.2))
.apply_filter(Wave::new(2.0, 20.0))
.view(query_params.width, query_params.height)
.apply_filter(
captcha::filters::Cow::new()
.min_radius(40)
.max_radius(50)
.circles(1)
.area(Geometry::new(40, 150, 50, 70)),
);
let image_buffer = match c.as_png() {
Some(data) => data,
None => {
return HttpResponse::InternalServerError().body("CAPTCHA generation failed");
}
};
let b64 = general_purpose::STANDARD.encode(&image_buffer);
if query_params.typ == "png" || query_params.typ.is_empty() {
HttpResponse::Ok()
.content_type("image/png")
.body(image_buffer)
} else {
HttpResponse::Ok()
.content_type("text/plain")
.body(format!("data:image/png;base64,{}", b64))
}
}
pub async fn full_reload(app_state: web::Data<AppContext>, request: HttpRequest) -> impl Responder {
let templates = &app_state.tpl;
let thread_data = std::sync::Arc::clone(templates);
let mut tera = thread_data.write().expect("Failed to acquire write lock");
if let Err(e) = tera.full_reload() {
return request.json(200, R::failed(500, e.to_string()));
}
std::env::set_var(
"STARTUP_TIME",
format!("{}", crate::commons::timestamp_millis()),
);
log::info!(
"STARTUP_TIME={}",
crate::commons::read_env("STARTUP_TIME", "")
);
request.json(
200,
R::success(true, "full_reload html templates successfully".to_string()),
)
}