- Remove all use slog::* imports and slog::Logger fields/parameters - Replace slog::info!/warn!/error! with tracing::info!/warn!/error! - AppService: remove pub logs: slog::Logger field, update callers of AppEmail::init(), MessageProducer::new(), RoomService::new(), start_email_worker(), start_room_workers() - auth/: captcha, email, login, logout, password, register, rsa, totp - git/: archive, blame, blob, branch, commit, contributors, diff, refs, star, tag, tree, watch - agent/: billing (ai_usage_recorded), code_review, pr_summary, sync - project/activity.rs, workspace/alert.rs
160 lines
5.6 KiB
Rust
160 lines
5.6 KiB
Rust
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use argon2::password_hash::{Salt, SaltString};
|
|
use argon2::{Argon2, PasswordHasher};
|
|
use models::users::{user, user_activity_log, user_email, user_password};
|
|
use models::workspaces::{WorkspaceRole, workspace, workspace_membership};
|
|
use sea_orm::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use session::Session;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
|
pub struct RegisterParams {
|
|
pub username: String,
|
|
pub email: String,
|
|
pub password: String,
|
|
pub captcha: String,
|
|
}
|
|
|
|
impl AppService {
|
|
pub async fn auth_register(
|
|
&self,
|
|
params: RegisterParams,
|
|
context: &Session,
|
|
) -> Result<user::Model, AppError> {
|
|
self.auth_check_captcha(&context, params.captcha).await?;
|
|
let password = self.auth_rsa_decode(&context, params.password).await?;
|
|
if self
|
|
.utils_find_user_by_username(params.username.clone())
|
|
.await
|
|
.is_ok()
|
|
{
|
|
tracing::warn!(username = %params.username, "Registration failed: username already exists");
|
|
return Err(AppError::UserNameExists);
|
|
}
|
|
|
|
if self
|
|
.utils_find_user_by_email(params.email.clone())
|
|
.await
|
|
.is_ok()
|
|
{
|
|
tracing::warn!(email = %params.email, "Registration failed: email already exists");
|
|
return Err(AppError::EmailExists);
|
|
}
|
|
|
|
let user_uid = Uuid::now_v7();
|
|
let now = chrono::Utc::now();
|
|
let txn = self.db.begin().await.map_err(|_| AppError::TxnError)?;
|
|
|
|
let user_model = user::ActiveModel {
|
|
uid: Set(user_uid),
|
|
username: Set(params.username.clone()),
|
|
display_name: Set(Some(params.username.clone())),
|
|
avatar_url: Set(None),
|
|
website_url: Set(None),
|
|
organization: Set(None),
|
|
last_sign_in_at: Set(None),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
|
|
let user = user_model.insert(&txn).await.map_err(|e| {
|
|
tracing::error!(error = ?e, "Failed to insert user");
|
|
AppError::UserNotFound
|
|
})?;
|
|
|
|
let user_email_model = user_email::ActiveModel {
|
|
user: Set(user_uid),
|
|
email: Set(params.email),
|
|
created_at: Set(now),
|
|
};
|
|
|
|
user_email_model.insert(&txn).await.map_err(|e| {
|
|
tracing::error!(error = ?e, "Failed to insert user email");
|
|
AppError::UserNotFound
|
|
})?;
|
|
|
|
let salt = SaltString::generate(&mut rsa::rand_core::OsRng::default());
|
|
let password_hash = Argon2::default()
|
|
.hash_password(password.as_bytes(), Salt::from_b64(&*salt.to_string())?)
|
|
.map_err(|e| {
|
|
tracing::error!(error = ?e, "Failed to hash password");
|
|
AppError::UserNotFound
|
|
})?
|
|
.to_string();
|
|
let user_password_model = user_password::ActiveModel {
|
|
user: Set(user_uid),
|
|
password_hash: Set(password_hash),
|
|
password_salt: Set(Some(salt.to_string())),
|
|
is_active: Set(true),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
|
|
user_password_model.insert(&txn).await.map_err(|e| {
|
|
tracing::error!(error = ?e, "Failed to insert user password");
|
|
AppError::UserNotFound
|
|
})?;
|
|
|
|
let _ = user_activity_log::ActiveModel {
|
|
user_uid: Set(Option::from(user_uid)),
|
|
action: Set("register".to_string()),
|
|
ip_address: Set(context.ip_address()),
|
|
user_agent: Set(context.user_agent()),
|
|
details: Set(serde_json::json!({
|
|
"username": user.username.clone(),
|
|
"method": "password"
|
|
})
|
|
.into()),
|
|
created_at: Set(now),
|
|
..Default::default()
|
|
}
|
|
.insert(&txn)
|
|
.await;
|
|
|
|
// Auto-create personal workspace for the new user
|
|
let personal_slug = format!("~{}", params.username);
|
|
let ws = workspace::ActiveModel {
|
|
id: Set(Uuid::now_v7()),
|
|
slug: Set(personal_slug),
|
|
name: Set(format!("{} 的工作空间", params.username)),
|
|
description: Set(None),
|
|
avatar_url: Set(None),
|
|
plan: Set("free".to_string()),
|
|
billing_email: Set(None),
|
|
stripe_customer_id: Set(None),
|
|
stripe_subscription_id: Set(None),
|
|
plan_expires_at: Set(None),
|
|
deleted_at: Set(None),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
let ws = ws.insert(&txn).await.map_err(|e| {
|
|
tracing::error!(error = ?e, "Failed to insert personal workspace");
|
|
AppError::UserNotFound
|
|
})?;
|
|
|
|
let _ = workspace_membership::ActiveModel {
|
|
id: Default::default(),
|
|
workspace_id: Set(ws.id),
|
|
user_id: Set(user_uid),
|
|
role: Set(WorkspaceRole::Owner.to_string()),
|
|
status: Set("active".to_string()),
|
|
invited_by: Set(None),
|
|
joined_at: Set(now),
|
|
invite_token: Set(None),
|
|
invite_expires_at: Set(None),
|
|
}
|
|
.insert(&txn)
|
|
.await;
|
|
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
|
context.set_user(user_uid);
|
|
context.set_current_workspace_id(ws.id);
|
|
context.remove(Self::RSA_PRIVATE_KEY);
|
|
context.remove(Self::RSA_PUBLIC_KEY);
|
|
tracing::info!(user_uid = %user_uid, username = %user.username, "User registered successfully");
|
|
Ok(user)
|
|
}
|
|
}
|