1 添加用户密码加密、验证
build / build-rust (push) Successful in 3m7s
Details
build / build-rust (push) Successful in 3m7s
Details
2 添加用户名、邮箱等是否已经存在接口
This commit is contained in:
parent
25a63b1fb7
commit
61de18077e
|
@ -125,6 +125,18 @@ version = "1.0.91"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
|
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arraydeque"
|
name = "arraydeque"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -602,6 +614,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
|
@ -1907,6 +1928,17 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
@ -2417,7 +2449,10 @@ name = "rtsa_db"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"argon2",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"md-5",
|
||||||
|
"rand_core",
|
||||||
"regex",
|
"regex",
|
||||||
"rtsa_dto",
|
"rtsa_dto",
|
||||||
"rtsa_log",
|
"rtsa_log",
|
||||||
|
|
|
@ -19,6 +19,9 @@ sqlx = { workspace = true, features = [
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
rand_core = { version = "0.6.4", features = ["std"] }
|
||||||
|
md-5 = "0.10.6"
|
||||||
|
|
||||||
rtsa_dto = { path = "../rtsa_dto" }
|
rtsa_dto = { path = "../rtsa_dto" }
|
||||||
rtsa_log = { path = "../rtsa_log" }
|
rtsa_log = { path = "../rtsa_log" }
|
||||||
|
|
|
@ -3,7 +3,6 @@ use sqlx::Postgres;
|
||||||
|
|
||||||
use super::RtsaDbAccessor;
|
use super::RtsaDbAccessor;
|
||||||
use super::{OrgAccessor, RegisterUser, UserAccessor};
|
use super::{OrgAccessor, RegisterUser, UserAccessor};
|
||||||
use crate::password_util::verify_password;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{PageData, PageQuery, TableColumn},
|
common::{PageData, PageQuery, TableColumn},
|
||||||
model::{OrganizationUserColumn, OrganizationUserModel, UserModel},
|
model::{OrganizationUserColumn, OrganizationUserModel, UserModel},
|
||||||
|
@ -48,14 +47,6 @@ pub trait OrgUserAccessor {
|
||||||
info: Value,
|
info: Value,
|
||||||
updater_id: i32,
|
updater_id: i32,
|
||||||
) -> Result<OrganizationUserModel, DbAccessError>;
|
) -> Result<OrganizationUserModel, DbAccessError>;
|
||||||
/// 查询组织用户登陆
|
|
||||||
/// username: 学工号/用户名/邮箱/手机号
|
|
||||||
async fn query_org_user_login(
|
|
||||||
&self,
|
|
||||||
org_id: i32,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
) -> Result<(OrganizationUserModel, UserModel), DbAccessError>;
|
|
||||||
/// 获取组织用户
|
/// 获取组织用户
|
||||||
async fn query_org_user(
|
async fn query_org_user(
|
||||||
&self,
|
&self,
|
||||||
|
@ -383,35 +374,6 @@ impl OrgUserAccessor for RtsaDbAccessor {
|
||||||
Ok(update)
|
Ok(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn query_org_user_login(
|
|
||||||
&self,
|
|
||||||
org_id: i32,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
) -> Result<(OrganizationUserModel, UserModel), DbAccessError> {
|
|
||||||
// 查询用户登陆
|
|
||||||
let user = self.query_user_login(username, password).await;
|
|
||||||
match user {
|
|
||||||
Err(_) => {
|
|
||||||
// 用户不存在,查询组织用户学工号+用户密码
|
|
||||||
// 通过组织id和学工号查询组织用户
|
|
||||||
let org_user = self.query_org_user_by_student_id(org_id, username).await?;
|
|
||||||
let user = self.query_user(org_user.user_id).await?;
|
|
||||||
// 检查用户密码
|
|
||||||
if verify_password(password, &user.password) {
|
|
||||||
Ok((org_user, user))
|
|
||||||
} else {
|
|
||||||
Err(DbAccessError::InvalidArgument("密码不匹配".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(user) => {
|
|
||||||
// 用户存在,查询组织用户
|
|
||||||
let org_user = self.query_org_user(org_id, user.id).await?;
|
|
||||||
Ok((org_user, user))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn query_org_user(
|
async fn query_org_user(
|
||||||
&self,
|
&self,
|
||||||
org_id: i32,
|
org_id: i32,
|
||||||
|
@ -594,43 +556,6 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
|
||||||
async fn test_query_org_user_login(pool: PgPool) -> Result<(), DbAccessError> {
|
|
||||||
let accessor = RtsaDbAccessor::new(pool);
|
|
||||||
let (creator, org) = init_default_org_and_user(&accessor).await?;
|
|
||||||
let create = CreateOrgUser::new(
|
|
||||||
&org.code.clone().unwrap(),
|
|
||||||
"test_student_id",
|
|
||||||
"test_name",
|
|
||||||
"test_password",
|
|
||||||
creator.id,
|
|
||||||
);
|
|
||||||
accessor.create_org_user(create.clone()).await?;
|
|
||||||
// Attempt to log in with student ID
|
|
||||||
let (org_user, user) = accessor
|
|
||||||
.query_org_user_login(org.id, "test_student_id", "test_password")
|
|
||||||
.await?;
|
|
||||||
assert_eq!(org_user.organization_id, org.id);
|
|
||||||
assert_eq!(user.nickname, create.nickname());
|
|
||||||
// Attempt to log in with username
|
|
||||||
let (org_user, user) = accessor
|
|
||||||
.query_org_user_login(org.id, &create.build_username(), "test_password")
|
|
||||||
.await?;
|
|
||||||
assert_eq!(org_user.organization_id, org.id);
|
|
||||||
assert_eq!(user.nickname, create.nickname());
|
|
||||||
// Attempt to log in with wrong password
|
|
||||||
let result = accessor
|
|
||||||
.query_org_user_login(org.id, "test_student_id", "wrong_password")
|
|
||||||
.await;
|
|
||||||
assert!(matches!(result, Err(DbAccessError::InvalidArgument(_))));
|
|
||||||
// Attempt to log in with wrong username
|
|
||||||
let result = accessor
|
|
||||||
.query_org_user_login(org.id, "wrong_student_id", "test_password")
|
|
||||||
.await;
|
|
||||||
assert!(matches!(result, Err(DbAccessError::SqlxError(_))));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
async fn test_query_org_user_by_student_id(pool: PgPool) -> Result<(), DbAccessError> {
|
async fn test_query_org_user_by_student_id(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
let accessor = RtsaDbAccessor::new(pool);
|
let accessor = RtsaDbAccessor::new(pool);
|
||||||
|
|
|
@ -4,7 +4,7 @@ use sqlx::Postgres;
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{PageData, PageQuery, TableColumn},
|
common::{PageData, PageQuery, TableColumn},
|
||||||
model::{UserColumn, UserModel},
|
model::{UserColumn, UserModel},
|
||||||
password_util::verify_password,
|
password_util::{self, verify_password},
|
||||||
username_util::{is_email, is_mobile},
|
username_util::{is_email, is_mobile},
|
||||||
DbAccessError,
|
DbAccessError,
|
||||||
};
|
};
|
||||||
|
@ -48,6 +48,10 @@ pub trait UserAccessor {
|
||||||
async fn query_user(&self, id: i32) -> Result<UserModel, DbAccessError>;
|
async fn query_user(&self, id: i32) -> Result<UserModel, DbAccessError>;
|
||||||
/// 根据username查询用户数据
|
/// 根据username查询用户数据
|
||||||
async fn query_user_by_username(&self, username: &str) -> Result<UserModel, DbAccessError>;
|
async fn query_user_by_username(&self, username: &str) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 根据email查询用户数据
|
||||||
|
async fn query_user_by_email(&self, email: &str) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 根据mobile查询用户数据
|
||||||
|
async fn query_user_by_mobile(&self, mobile: &str) -> Result<UserModel, DbAccessError>;
|
||||||
/// 是否用户名已经存在
|
/// 是否用户名已经存在
|
||||||
async fn is_user_name_exist(&self, name: &str) -> Result<bool, DbAccessError>;
|
async fn is_user_name_exist(&self, name: &str) -> Result<bool, DbAccessError>;
|
||||||
/// 是否用户email已经存在
|
/// 是否用户email已经存在
|
||||||
|
@ -296,7 +300,8 @@ impl UserAccessor for RtsaDbAccessor {
|
||||||
Ok(PageData::new(total, rows))
|
Ok(PageData::new(total, rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn register_user(&self, user: RegisterUser) -> Result<UserModel, DbAccessError> {
|
async fn register_user(&self, mut user: RegisterUser) -> Result<UserModel, DbAccessError> {
|
||||||
|
user.password = password_util::hash_password(&user.password)?;
|
||||||
self.insert_user(user, &self.pool).await
|
self.insert_user(user, &self.pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,88 +310,34 @@ impl UserAccessor for RtsaDbAccessor {
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<UserModel, DbAccessError> {
|
) -> Result<UserModel, DbAccessError> {
|
||||||
let table = UserColumn::Table.name();
|
let user: UserModel;
|
||||||
let username_column = UserColumn::Username.name();
|
|
||||||
let email_column = UserColumn::Email.name();
|
|
||||||
let mobile_column = UserColumn::Mobile.name();
|
|
||||||
if is_email(username) {
|
if is_email(username) {
|
||||||
let query_clause = format!(
|
let query_result = self.query_user_by_email(username).await;
|
||||||
"SELECT * FROM {table} WHERE {email_column} = $1 LIMIT 1",
|
if query_result.is_ok() {
|
||||||
table = table,
|
user = query_result.unwrap();
|
||||||
email_column = email_column,
|
} else {
|
||||||
);
|
return Err(DbAccessError::UserNotExist("email".to_string()));
|
||||||
let user: Result<UserModel, sqlx::Error> = sqlx::query_as(&query_clause)
|
|
||||||
.bind(username)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await;
|
|
||||||
match user {
|
|
||||||
Ok(user) => {
|
|
||||||
if verify_password(password, &user.password) {
|
|
||||||
Ok(user)
|
|
||||||
} else {
|
|
||||||
Err(DbAccessError::InvalidArgument("密码不匹配".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(sqlx::Error::RowNotFound) => Err(DbAccessError::InvalidArgument(
|
|
||||||
"用户不存在(email)".to_string(),
|
|
||||||
)),
|
|
||||||
Err(e) => Err(DbAccessError::SqlxError(e)),
|
|
||||||
}
|
}
|
||||||
} else if is_mobile(username) {
|
} else if is_mobile(username) {
|
||||||
let query_clause = format!(
|
let query_result = self.query_user_by_mobile(username).await;
|
||||||
"SELECT * FROM {table} WHERE {mobile_column} = $1 LIMIT 1",
|
if query_result.is_ok() {
|
||||||
table = table,
|
user = query_result.unwrap();
|
||||||
mobile_column = mobile_column,
|
} else {
|
||||||
);
|
return Err(DbAccessError::UserNotExist("mobile".to_string()));
|
||||||
let user: Result<UserModel, sqlx::Error> = sqlx::query_as(&query_clause)
|
|
||||||
.bind(username)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await;
|
|
||||||
match user {
|
|
||||||
Ok(user) => {
|
|
||||||
if verify_password(password, &user.password) {
|
|
||||||
return Ok(user);
|
|
||||||
} else {
|
|
||||||
return Err(DbAccessError::InvalidArgument("密码不匹配".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
|
||||||
return Err(DbAccessError::InvalidArgument(
|
|
||||||
"用户不存在(mobile)".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(DbAccessError::SqlxError(e));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let query_clause = format!(
|
let query_result = self.query_user_by_username(username).await;
|
||||||
"SELECT * FROM {table} WHERE {username_column} = $1 LIMIT 1",
|
if query_result.is_ok() {
|
||||||
table = table,
|
user = query_result.unwrap();
|
||||||
username_column = username_column,
|
} else {
|
||||||
);
|
return Err(DbAccessError::UserNotExist("username".to_string()));
|
||||||
let user: Result<UserModel, sqlx::Error> = sqlx::query_as(&query_clause)
|
|
||||||
.bind(username)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await;
|
|
||||||
match user {
|
|
||||||
Ok(user) => {
|
|
||||||
if verify_password(password, &user.password) {
|
|
||||||
return Ok(user);
|
|
||||||
} else {
|
|
||||||
return Err(DbAccessError::InvalidArgument("密码不匹配".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
|
||||||
return Err(DbAccessError::InvalidArgument(
|
|
||||||
"用户不存在(username)".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(DbAccessError::SqlxError(e));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if verify_password(password, &user.password).is_ok() {
|
||||||
|
Ok(user)
|
||||||
|
} else {
|
||||||
|
Err(DbAccessError::PasswordNotMatch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn query_user(&self, id: i32) -> Result<UserModel, DbAccessError> {
|
async fn query_user(&self, id: i32) -> Result<UserModel, DbAccessError> {
|
||||||
|
@ -415,6 +366,36 @@ impl UserAccessor for RtsaDbAccessor {
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn query_user_by_email(&self, email: &str) -> Result<UserModel, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let email_column = UserColumn::Email.name();
|
||||||
|
let query_clause = format!(
|
||||||
|
"SELECT * FROM {table} WHERE {email_column} = $1 LIMIT 1",
|
||||||
|
table = table,
|
||||||
|
email_column = email_column,
|
||||||
|
);
|
||||||
|
let user = sqlx::query_as(&query_clause)
|
||||||
|
.bind(email)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_user_by_mobile(&self, mobile: &str) -> Result<UserModel, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let mobile_column = UserColumn::Mobile.name();
|
||||||
|
let query_clause = format!(
|
||||||
|
"SELECT * FROM {table} WHERE {mobile_column} = $1 LIMIT 1",
|
||||||
|
table = table,
|
||||||
|
mobile_column = mobile_column,
|
||||||
|
);
|
||||||
|
let user = sqlx::query_as(&query_clause)
|
||||||
|
.bind(mobile)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_user_info(&self, id: i32, info: Value) -> Result<UserModel, DbAccessError> {
|
async fn update_user_info(&self, id: i32, info: Value) -> Result<UserModel, DbAccessError> {
|
||||||
let table = UserColumn::Table.name();
|
let table = UserColumn::Table.name();
|
||||||
let id_column = UserColumn::Id.name();
|
let id_column = UserColumn::Id.name();
|
||||||
|
@ -451,15 +432,16 @@ impl UserAccessor for RtsaDbAccessor {
|
||||||
"new_password is too long".to_string(),
|
"new_password is too long".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let hashed = password_util::hash_password(new_password)?;
|
||||||
let table = UserColumn::Table.name();
|
let table = UserColumn::Table.name();
|
||||||
let id_column = UserColumn::Id.name();
|
let id_column = UserColumn::Id.name();
|
||||||
let password_column = UserColumn::Password.name();
|
let password_column = UserColumn::Password.name();
|
||||||
let updated_at_column = UserColumn::UpdatedAt.name();
|
let updated_at_column = UserColumn::UpdatedAt.name();
|
||||||
let update_clause = format!(
|
let update_clause = format!(
|
||||||
"UPDATE {table} SET {password_column} = '{new_password}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
|
"UPDATE {table} SET {password_column} = '{hashed}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
|
||||||
table = table,
|
table = table,
|
||||||
password_column = password_column,
|
password_column = password_column,
|
||||||
new_password = new_password,
|
hashed = hashed,
|
||||||
id_column = id_column,
|
id_column = id_column,
|
||||||
id = id
|
id = id
|
||||||
);
|
);
|
||||||
|
@ -609,12 +591,14 @@ impl UserAccessor for RtsaDbAccessor {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use rtsa_dto::common::Role;
|
use rtsa_dto::common::Role;
|
||||||
|
use rtsa_log::tracing::info;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
async fn test_register_user(pool: PgPool) -> Result<(), DbAccessError> {
|
async fn test_register_user(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
rtsa_log::Logging::default().init();
|
||||||
let accessor = RtsaDbAccessor { pool };
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
let new_user = RegisterUser::new("test_user", "test_password")
|
let new_user = RegisterUser::new("test_user", "test_password")
|
||||||
|
@ -627,6 +611,7 @@ mod tests {
|
||||||
let queried_user = accessor
|
let queried_user = accessor
|
||||||
.query_user_login("test_user@example.com", "test_password")
|
.query_user_login("test_user@example.com", "test_password")
|
||||||
.await?;
|
.await?;
|
||||||
|
info!("queried_user: {:?}", queried_user);
|
||||||
assert_eq!(queried_user.username, "test_user");
|
assert_eq!(queried_user.username, "test_user");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
queried_user.email,
|
queried_user.email,
|
||||||
|
@ -781,6 +766,42 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user_by_email(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password").with_email("test@test.cn");
|
||||||
|
let um = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Assume a user with email exists
|
||||||
|
let user = accessor.query_user_by_email("test@test.cn").await?;
|
||||||
|
assert_eq!(user.id, um.id);
|
||||||
|
|
||||||
|
// Assume a user with email does not exist
|
||||||
|
let result = accessor.query_user_by_email("dev@test.cn").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user_by_mobile(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password").with_mobile("13345678901");
|
||||||
|
let um = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Assume a user with mobile exists
|
||||||
|
let user = accessor.query_user_by_mobile("13345678901").await?;
|
||||||
|
assert_eq!(user.id, um.id);
|
||||||
|
|
||||||
|
// Assume a user with mobile does not exist
|
||||||
|
let result = accessor.query_user_by_mobile("13345678902").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
async fn test_query_user_login(pool: PgPool) -> Result<(), DbAccessError> {
|
async fn test_query_user_login(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
let accessor = RtsaDbAccessor { pool };
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
@ -809,7 +830,7 @@ mod tests {
|
||||||
|
|
||||||
// Test with incorrect credentials
|
// Test with incorrect credentials
|
||||||
let result = accessor
|
let result = accessor
|
||||||
.query_user_login("test_user@example.com", "wrongpassword")
|
.query_user_login("test_user@example.com", "wrong_password")
|
||||||
.await;
|
.await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -14,4 +14,8 @@ pub enum DbAccessError {
|
||||||
DataError(String),
|
DataError(String),
|
||||||
#[error("非法参数:{0}")]
|
#[error("非法参数:{0}")]
|
||||||
InvalidArgument(String),
|
InvalidArgument(String),
|
||||||
|
#[error("用户不存在:{0}")]
|
||||||
|
UserNotExist(String),
|
||||||
|
#[error("密码不正确")]
|
||||||
|
PasswordNotMatch,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,69 @@
|
||||||
|
use argon2::{
|
||||||
|
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use md5::{Digest, Md5};
|
||||||
|
|
||||||
|
use crate::DbAccessError;
|
||||||
|
|
||||||
/// 验证密码是否正确
|
/// 验证密码是否正确
|
||||||
pub fn verify_password(password: &str, hash: &str) -> bool {
|
pub fn verify_password(password: &str, hash: &str) -> Result<(), DbAccessError> {
|
||||||
password == hash
|
// Verify password against PHC string.
|
||||||
|
// NOTE: hash params from `parsed_hash` are used instead of what is configured in the
|
||||||
|
// `Argon2` instance.
|
||||||
|
let parsed_hash = PasswordHash::new(hash);
|
||||||
|
match parsed_hash {
|
||||||
|
Ok(parsed_hash) => Argon2::default()
|
||||||
|
.verify_password(password.as_bytes(), &parsed_hash)
|
||||||
|
.map_err(|e| DbAccessError::InvalidArgument(format!("password verify error: {}", e))),
|
||||||
|
Err(e) => Err(DbAccessError::InvalidArgument(format!(
|
||||||
|
"password hash error: {}",
|
||||||
|
e
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash_password(password: &str) -> Result<String, DbAccessError> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
|
||||||
|
// Argon2 with default params (Argon2id v19)
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
|
// Hash password to PHC string ($argon2id$v=19$...)
|
||||||
|
let password_hash = argon2.hash_password(password.as_bytes(), &salt);
|
||||||
|
match password_hash {
|
||||||
|
Ok(ph) => Ok(ph.to_string()),
|
||||||
|
Err(e) => Err(DbAccessError::InvalidArgument(format!(
|
||||||
|
"password hash error: {}",
|
||||||
|
e
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn md5(input: &str) -> String {
|
||||||
|
let mut hasher = Md5::new();
|
||||||
|
hasher.update(input);
|
||||||
|
let result = hasher.finalize();
|
||||||
|
format!("{:x}", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash_password() {
|
||||||
|
let password = "password";
|
||||||
|
let hash = hash_password(password).unwrap();
|
||||||
|
assert!(verify_password(password, &hash).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_md5() {
|
||||||
|
let input = "hello";
|
||||||
|
let output = md5(input);
|
||||||
|
assert_eq!(output, "5d41402abc4b2a76b9719d911017c592");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,35 @@ impl UserQuery {
|
||||||
Ok(user.into())
|
Ok(user.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 用户名是否存在
|
||||||
|
async fn username_exists(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
username: String,
|
||||||
|
) -> async_graphql::Result<bool> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let exist = db_accessor.is_user_name_exist(&username).await?;
|
||||||
|
Ok(exist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 邮箱是否存在
|
||||||
|
async fn email_exists(&self, ctx: &Context<'_>, email: String) -> async_graphql::Result<bool> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let exist = db_accessor.is_user_email_exist(&email).await?;
|
||||||
|
Ok(exist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 手机号是否存在
|
||||||
|
async fn mobile_exists(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
mobile: String,
|
||||||
|
) -> async_graphql::Result<bool> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let exist = db_accessor.is_user_mobile_exist(&mobile).await?;
|
||||||
|
Ok(exist)
|
||||||
|
}
|
||||||
|
|
||||||
/// 分页查询用户(系统管理)
|
/// 分页查询用户(系统管理)
|
||||||
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
async fn user_paging(
|
async fn user_paging(
|
||||||
|
|
|
@ -6,7 +6,7 @@ use rtsa_log::tracing::{debug, error, info};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub const ADMIN_USER_NAME: &str = "_jl_admin_";
|
pub const ADMIN_USER_NAME: &str = "_jl_admin_";
|
||||||
pub const ADMIN_USER_PASSWORD: &str = "joylink0503";
|
pub const ADMIN_USER_PASSWORD: &str = "4a6d74126bfd06d69406fcccb7e7d5d9";
|
||||||
|
|
||||||
pub const DEFAULT_ORG_CODE: &str = "default";
|
pub const DEFAULT_ORG_CODE: &str = "default";
|
||||||
const DEFAULT_ORG_NAME: &str = "默认机构";
|
const DEFAULT_ORG_NAME: &str = "默认机构";
|
||||||
|
@ -64,7 +64,6 @@ mod tests {
|
||||||
let db_accessor = rtsa_db::get_default_db_accessor();
|
let db_accessor = rtsa_db::get_default_db_accessor();
|
||||||
let user = db_accessor.query_user_by_username(ADMIN_USER_NAME).await?;
|
let user = db_accessor.query_user_by_username(ADMIN_USER_NAME).await?;
|
||||||
assert_eq!(user.username, ADMIN_USER_NAME);
|
assert_eq!(user.username, ADMIN_USER_NAME);
|
||||||
assert_eq!(user.password, ADMIN_USER_PASSWORD);
|
|
||||||
assert_eq!(user.nickname, ADMIN_USER_NAME);
|
assert_eq!(user.nickname, ADMIN_USER_NAME);
|
||||||
assert_eq!(user.roles, json!([Role::Admin]));
|
assert_eq!(user.roles, json!([Role::Admin]));
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use async_graphql::Guard;
|
use async_graphql::Guard;
|
||||||
use rtsa_db::prelude::*;
|
use rtsa_db::prelude::*;
|
||||||
use rtsa_dto::common::Role;
|
use rtsa_dto::common::Role;
|
||||||
use rtsa_log::tracing::warn;
|
use rtsa_log::tracing::{info, warn};
|
||||||
|
|
||||||
mod jwt_auth;
|
mod jwt_auth;
|
||||||
use crate::{apis::UserLoginDto, error::BusinessError, sys_init::DEFAULT_ORG_CODE};
|
use crate::{apis::UserLoginDto, error::BusinessError, sys_init::DEFAULT_ORG_CODE};
|
||||||
|
@ -53,10 +53,10 @@ impl Guard for RoleGuard {
|
||||||
/// 处理用户登录
|
/// 处理用户登录
|
||||||
pub(crate) async fn handle_login(
|
pub(crate) async fn handle_login(
|
||||||
db_accessor: &RtsaDbAccessor,
|
db_accessor: &RtsaDbAccessor,
|
||||||
info: UserLoginDto,
|
user_login_dto: UserLoginDto,
|
||||||
) -> Result<Jwt, BusinessError> {
|
) -> Result<Jwt, BusinessError> {
|
||||||
let org_id;
|
let org_id;
|
||||||
if let Some(oid) = info.org_id {
|
if let Some(oid) = user_login_dto.org_id {
|
||||||
org_id = oid;
|
org_id = oid;
|
||||||
} else {
|
} else {
|
||||||
let default_org = db_accessor.query_org_by_code(DEFAULT_ORG_CODE).await?;
|
let default_org = db_accessor.query_org_by_code(DEFAULT_ORG_CODE).await?;
|
||||||
|
@ -64,35 +64,57 @@ pub(crate) async fn handle_login(
|
||||||
}
|
}
|
||||||
// 查询用户登陆
|
// 查询用户登陆
|
||||||
let user = db_accessor
|
let user = db_accessor
|
||||||
.query_user_login(&info.username, &info.password)
|
.query_user_login(&user_login_dto.username, &user_login_dto.password)
|
||||||
.await;
|
.await;
|
||||||
match user {
|
match user {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
// 用户存在
|
// 用户存在
|
||||||
Ok(Jwt::build_from(Claims::new(user.id, org_id))?)
|
Ok(Jwt::build_from(Claims::new(user.id, org_id))?)
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
// 用户不存在,查询组织用户学工号+用户密码
|
match e {
|
||||||
// 通过组织id和学工号查询组织用户
|
DbAccessError::UserNotExist(e) => {
|
||||||
let org_user = db_accessor
|
info!(
|
||||||
.query_org_user_by_student_id(org_id, &info.username)
|
"用户不存在: {}, 尝试查询组织学工号用户: username={}, org_id={}",
|
||||||
.await;
|
e, user_login_dto.username, org_id
|
||||||
if org_user.is_err() {
|
);
|
||||||
warn!("用户不存在: username={}, org_id={}", info.username, org_id);
|
// 用户不存在,查询组织用户学工号+用户密码
|
||||||
return Err(BusinessError::AuthError(
|
// 通过组织id和学工号查询组织用户
|
||||||
|
let org_user = db_accessor
|
||||||
|
.query_org_user_by_student_id(org_id, &user_login_dto.username)
|
||||||
|
.await;
|
||||||
|
if org_user.is_err() {
|
||||||
|
warn!(
|
||||||
|
"用户不存在: username={}, org_id={}",
|
||||||
|
user_login_dto.username, org_id
|
||||||
|
);
|
||||||
|
return Err(BusinessError::AuthError(
|
||||||
|
"用户不存在或密码不正确".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let org_user = org_user.unwrap();
|
||||||
|
let user = db_accessor.query_user(org_user.user_id).await?;
|
||||||
|
// 检查用户密码
|
||||||
|
if rtsa_db::password_util::verify_password(
|
||||||
|
&user_login_dto.password,
|
||||||
|
&user.password,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
Ok(Jwt::build_from(Claims::new(user.id, org_id))?)
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"密码不匹配: username={}, org_id={}",
|
||||||
|
user_login_dto.username, org_id
|
||||||
|
);
|
||||||
|
Err(BusinessError::AuthError(
|
||||||
|
"用户不存在或密码不正确".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(BusinessError::AuthError(
|
||||||
"用户不存在或密码不正确".to_string(),
|
"用户不存在或密码不正确".to_string(),
|
||||||
));
|
)),
|
||||||
}
|
|
||||||
let org_user = org_user.unwrap();
|
|
||||||
let user = db_accessor.query_user(org_user.user_id).await?;
|
|
||||||
// 检查用户密码
|
|
||||||
if rtsa_db::password_util::verify_password(&info.password, &user.password) {
|
|
||||||
Ok(Jwt::build_from(Claims::new(user.id, org_id))?)
|
|
||||||
} else {
|
|
||||||
warn!("密码不匹配: username={}, org_id={}", info.username, org_id);
|
|
||||||
Err(BusinessError::AuthError(
|
|
||||||
"用户不存在或密码不正确".to_string(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue