use std::collections::HashMap;
use std::sync::Arc;
use super::model::login_model::{LoginConfig, LoginParam, LoginToken};
use crate::cache::actor_model::{CacheManagerRaftResult, CacheSetParam};
use crate::ldap::model::actor_model::{LdapMsgReq, LdapMsgResult};
use crate::ldap::model::LdapUserParam;
use crate::oauth2::model::actor_model::{OAuth2MsgReq, OAuth2MsgResult};
use crate::oauth2::model::OAuth2UserParam;
use crate::raft::store::{ClientRequest, ClientResponse};
use crate::{
common::{
appdata::AppShareData,
crypto_utils,
model::{ApiResult, UserSession},
},
now_second_i32,
raft::cache::{
model::{CacheKey, CacheType},
CacheLimiterReq,
},
user::{UserManagerReq, UserManagerResult},
};
use actix_web::http::{header, StatusCode};
use actix_web::{
cookie::Cookie,
web::{self, Data},
HttpRequest, HttpResponse, Responder,
};
use captcha::filters::{Grid, Noise};
use captcha::Captcha;
use serde::Deserialize;
pub async fn login(
request: HttpRequest,
app: Data<Arc<AppShareData>>,
web::Form(param): web::Form<LoginParam>,
) -> HttpResponse {
let captcha_token = if let Some(ck) = request.cookie("captcha_token") {
ck.value().to_owned()
} else {
String::new()
};
if app.sys_config.console_captcha_enable {
if let Some(value) = check_captcha(
&app,
param.captcha.clone().unwrap_or_default().to_uppercase(),
&captcha_token,
)
.await
{
return value;
}
}
let limit_key = Arc::new(format!("USER_L#{}", ¶m.username));
if let Some(value) = login_limit(&app, &limit_key).await {
return value;
}
let password = match decode_password(¶m.password, &captcha_token) {
Ok(v) => v,
Err(e) => {
log::error!("decode_password error:{}", e);
return HttpResponse::Ok().json(ApiResult::<()>::error(
"SYSTEM_ERROR".to_owned(),
Some("decode_password error".to_owned()),
));
}
};
let mut session = None;
let msg = UserManagerReq::CheckUser {
name: param.username.clone(),
password: password.clone(),
};
let mut error_code = "USER_CHECK_ERROR".to_owned();
if let Ok(Ok(res)) = app.user_manager.send(msg).await {
if let UserManagerResult::CheckUserResult(valid, user) = res {
if valid {
session = Some(Arc::new(UserSession {
username: user.username,
nickname: user.nickname,
roles: user.roles.unwrap_or_default(),
extend_infos: user.extend_info.unwrap_or_default(),
namespace_privilege: user.namespace_privilege,
refresh_time: now_second_i32() as u32,
}));
}
}
if !app.sys_config.ldap_enable && !app.sys_config.oauth2_enable && session.is_none() {
return HttpResponse::Ok().json(ApiResult::<()>::error(error_code, None));
}
} else {
error_code = "SYSTEM_ERROR".to_owned();
}
if session.is_none() && app.sys_config.ldap_enable {
match ldap_login(&app, param.clone(), password).await {
Ok(v) => {
session = v;
}
Err(e) => {
return HttpResponse::Ok()
.json(ApiResult::<()>::error(error_code, Some(e.to_string())));
}
}
}
if let Some(session) = session {
match apply_session(app, limit_key, session).await {
Ok(value) => {
return value;
}
Err(e) => {
return HttpResponse::Ok().json(ApiResult::<()>::error(
"SAVE_SESSION_ERROR".to_string(),
Some(e.to_string()),
));
}
}
}
HttpResponse::Ok().json(ApiResult::<()>::error(error_code, None))
}
#[derive(Deserialize)]
pub struct OAuth2CallbackQuery {
code: String,
state: Option<String>,
}
pub async fn oauth2_callback(
_request: HttpRequest,
app: Data<Arc<AppShareData>>,
param: web::Json<OAuth2CallbackQuery>,
) -> HttpResponse {
if !app.sys_config.oauth2_enable {
return HttpResponse::Ok()
.status(StatusCode::NOT_FOUND)
.json(ApiResult::<()>::error(
"OAUTH2_NOT_ENABLED".to_owned(),
None,
));
}
let limit_key = Arc::new(format!("USER_L#oauth2"));
if let Some(value) = login_limit(&app, &limit_key).await {
return value;
}
let res = app
.oauth2_manager
.send(OAuth2MsgReq::Authenticate(OAuth2UserParam {
code: param.code.clone(),
state: param.state.clone(),
}))
.await;
let session = match res {
Ok(Ok(OAuth2MsgResult::UserMeta(meta))) => Some(Arc::new(UserSession {
username: Arc::new(meta.user_name.clone()),
nickname: Some(meta.user_name),
roles: vec![meta.role],
namespace_privilege: meta.namespace_privilege,
extend_infos: HashMap::default(),
refresh_time: now_second_i32() as u32,
})),
Ok(Ok(OAuth2MsgResult::None)) => None,
Ok(Ok(OAuth2MsgResult::AuthorizeUrl(_))) => {
return HttpResponse::Ok().json(ApiResult::<()>::error(
"OAUTH2_AUTH_ERROR".to_owned(),
Some("Unexpected response type".to_owned()),
));
}
Ok(Err(e)) => {
log::error!("OAuth2 authentication error: {}", e);
return HttpResponse::Ok().json(ApiResult::<()>::error(
"OAUTH2_AUTH_ERROR".to_owned(),
Some(e.to_string()),
));
}
Err(e) => {
log::error!("OAuth2 manager error: {}", e);
return HttpResponse::Ok().json(ApiResult::<()>::error(
"SYSTEM_ERROR".to_owned(),
Some(e.to_string()),
));
}
};
if let Some(session) = session {
match apply_session(app, limit_key, session).await {
Ok(value) => {
return value;
}
Err(e) => {
return HttpResponse::Ok().json(ApiResult::<()>::error(
"SAVE_SESSION_ERROR".to_string(),
Some(e.to_string()),
));
}
}
}
HttpResponse::Ok().json(ApiResult::<()>::error("OAUTH2_AUTH_ERROR".to_owned(), None))
}
pub async fn get_login_config(app: Data<Arc<AppShareData>>) -> actix_web::Result<impl Responder> {
let mut oauth2_button = None;
let mut oauth2_authorize_url = None;
if app.sys_config.oauth2_enable && !app.sys_config.oauth2_button.is_empty() {
oauth2_button = Some(app.sys_config.oauth2_button.as_ref().clone());
let res = app.oauth2_manager.send(OAuth2MsgReq::GetAuthorizeUrl).await;
if let Ok(Ok(OAuth2MsgResult::AuthorizeUrl(auth_url))) = res {
oauth2_authorize_url = Some(auth_url);
}
}
let config = LoginConfig {
oauth2_enable: app.sys_config.oauth2_enable,
oauth2_button,
oauth2_authorize_url,
};
Ok(HttpResponse::Ok().json(ApiResult::success(Some(config))))
}
async fn apply_session(
app: Data<Arc<AppShareData>>,
limit_key: Arc<String>,
session: Arc<UserSession>,
) -> anyhow::Result<HttpResponse> {
let token = Arc::new(
uuid::Uuid::new_v4().to_string().replace('-', "")
+ &uuid::Uuid::new_v4().to_string().replace('-', ""),
);
let cache_req =
crate::cache::actor_model::CacheManagerRaftReq::Set(CacheSetParam::new_with_ttl(
CacheKey::new(CacheType::UserSession, token.clone()),
crate::cache::model::CacheValue::UserSession(session),
app.sys_config.console_login_timeout,
));
app.raft_request_route
.request(ClientRequest::CacheReq { req: cache_req })
.await?;
let clear_limit_req = crate::cache::actor_model::CacheManagerRaftReq::Remove(CacheKey::new(
CacheType::String,
limit_key,
));
app.raft_request_route
.request(ClientRequest::CacheReq {
req: clear_limit_req,
})
.await
.ok();
let login_token = LoginToken {
token: token.to_string(),
};
Ok(HttpResponse::Ok()
.cookie(
Cookie::build("token", token.as_str())
.path("/")
.max_age(actix_web::cookie::time::Duration::seconds(
app.sys_config.console_login_timeout as i64,
))
.finish(),
)
.cookie(
Cookie::build("captcha_token", "")
.path("/")
.http_only(true)
.finish(),
)
.insert_header(header::ContentType(mime::APPLICATION_JSON))
.json(ApiResult::success(Some(login_token))))
}
async fn ldap_login(
app: &Data<Arc<AppShareData>>,
param: LoginParam,
password: String,
) -> anyhow::Result<Option<Arc<UserSession>>> {
let res = app
.ldap_manager
.send(LdapMsgReq::Bind(LdapUserParam {
user_name: param.username.as_ref().to_owned(),
password,
query_meta: true,
}))
.await??;
let v = if let LdapMsgResult::UserMeta(meta) = res {
Some(Arc::new(UserSession {
username: param.username.clone(),
nickname: Some(meta.user_name),
roles: vec![meta.role],
namespace_privilege: meta.namespace_privilege,
extend_infos: HashMap::default(),
refresh_time: now_second_i32() as u32,
}))
} else {
None
};
Ok(v)
}
async fn login_limit(
app: &Data<Arc<AppShareData>>,
limit_key: &Arc<String>,
) -> Option<HttpResponse> {
let limit_req = CacheLimiterReq::Hour {
key: limit_key.clone(),
limit: app.sys_config.console_login_one_hour_limit as i32,
};
let cache_req = crate::cache::actor_model::CacheManagerRaftReq::Limit(limit_req);
if let Ok(ClientResponse::CacheResp {
resp: CacheManagerRaftResult::Limiter(acquire_result),
}) = app
.raft_request_route
.request(ClientRequest::CacheReq { req: cache_req })
.await
{
if !acquire_result {
return Some(HttpResponse::Ok().json(ApiResult::<()>::error(
"LOGIN_LIMITE_ERROR".to_owned(),
Some("Frequent login, please try again later".to_owned()),
)));
}
} else {
return Some(
HttpResponse::Ok().json(ApiResult::<()>::error("SYSTEM_ERROR".to_owned(), None)),
);
}
None
}
async fn check_captcha(
app: &Data<Arc<AppShareData>>,
captcha_code: String,
captcha_token: &String,
) -> Option<HttpResponse> {
if captcha_token.is_empty() {
return Some(HttpResponse::Ok().json(ApiResult::<()>::error(
"CAPTCHA_CHECK_ERROR".to_owned(),
Some("captcha token is empty".to_owned()),
)));
}
let req = crate::cache::actor_model::CacheManagerLocalReq::Get(CacheKey::new(
CacheType::String,
Arc::new(format!("Captcha_{}", &captcha_token)),
));
let captcha_check_result =
if let Ok(Ok(CacheManagerRaftResult::Value(crate::cache::model::CacheValue::String(v)))) =
app.direct_cache_manager.send(req).await
{
&captcha_code == v.as_ref()
} else {
false
};
if !captcha_check_result {
return Some(
HttpResponse::Ok()
.cookie(
Cookie::build("captcha_token", "")
.path("/")
.http_only(true)
.finish(),
)
.json(ApiResult::<()>::error(
"CAPTCHA_CHECK_ERROR".to_owned(),
Some("CAPTCHA_CHECK_ERROR".to_owned()),
)),
);
}
None
}
fn decode_password(password: &str, captcha_token: &str) -> anyhow::Result<String> {
let password_data = crypto_utils::decode_base64(password)?;
if captcha_token.is_empty() {
let password = String::from_utf8(password_data)?;
Ok(password)
} else {
let password = String::from_utf8(crypto_utils::decrypt_aes128(
&captcha_token[0..16],
&captcha_token[16..32],
&password_data,
)?)?;
Ok(password)
}
}
const WIDTH: u32 = 220;
const HEIGHT: u32 = 120;
pub async fn gen_captcha(app: Data<Arc<AppShareData>>) -> actix_web::Result<impl Responder> {
let token = uuid::Uuid::new_v4().to_string().replace('-', "");
let captcha_cookie = Cookie::build("captcha_token", token.as_str())
.path("/")
.http_only(true)
.finish();
let captcha_header = ("Captcha-Token", token.as_str());
if !app.sys_config.console_captcha_enable {
return Ok(HttpResponse::Ok()
.cookie(captcha_cookie)
.insert_header(captcha_header)
.json(ApiResult::<String>::success(None)));
}
let mut obj = Captcha::new();
obj.add_chars(4)
.apply_filter(Noise::new(0.1))
.apply_filter(Grid::new(8, 8))
.view(WIDTH, HEIGHT);
let code: String = obj.chars().iter().collect::<String>().to_uppercase();
let code = Arc::new(code);
let img = obj.as_base64().unwrap_or_default();
let cache_req =
crate::cache::actor_model::CacheManagerRaftReq::Set(CacheSetParam::new_with_ttl(
CacheKey::new(CacheType::String, Arc::new(format!("Captcha_{}", &token))),
crate::cache::model::CacheValue::String(code),
300,
));
app.raft_request_route
.request(ClientRequest::CacheReq { req: cache_req })
.await
.ok();
Ok(HttpResponse::Ok()
.cookie(captcha_cookie)
.insert_header(captcha_header)
.json(ApiResult::success(Some(img))))
}
pub async fn logout(
request: HttpRequest,
app: Data<Arc<AppShareData>>,
) -> actix_web::Result<impl Responder> {
let token = if let Some(ck) = request.cookie("token") {
ck.value().to_owned()
} else if let Some(v) = request.headers().get("Token") {
v.to_str().unwrap_or_default().to_owned()
} else {
"".to_owned()
};
let token = Arc::new(token);
let cache_req = crate::cache::actor_model::CacheManagerRaftReq::Remove(CacheKey::new(
CacheType::UserSession,
token,
));
app.raft_request_route
.request(ClientRequest::CacheReq { req: cache_req })
.await
.ok();
Ok(HttpResponse::Ok()
.cookie(
Cookie::build("token", "")
.path("/")
.http_only(true)
.finish(),
)
.json(ApiResult::success(Some(true))))
}