backend signup
This commit is contained in:
+114
-49
@@ -27,12 +27,13 @@ use crate::{
|
||||
models::{NewRefreshToken, NewUser, RefreshToken, Tenant, TenantStatus, User, UserMembership},
|
||||
schema::{
|
||||
refresh_tokens, tenants::dsl as tenant_dsl, user_memberships::dsl as memberships_dsl,
|
||||
users::dsl,
|
||||
user_passkeys::dsl as passkey_dsl, users::dsl,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use crate::schema::refresh_tokens::dsl as refresh_dsl;
|
||||
use webauthn_rs::prelude::RegisterPublicKeyCredential;
|
||||
|
||||
const REFRESH_COOKIE_NAME: &str = "refresh_token";
|
||||
|
||||
@@ -44,13 +45,7 @@ pub struct LoginRequest {
|
||||
pub preferred_tenant_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SignupRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
@@ -58,7 +53,7 @@ pub struct LoginResponse {
|
||||
pub tenant: TenantSnippet,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TenantSnippet {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
@@ -80,48 +75,22 @@ pub struct TenantSelectionRequest {
|
||||
pub tenant_id: Uuid,
|
||||
}
|
||||
|
||||
pub async fn signup(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SignupRequest>,
|
||||
) -> AppResult<Response> {
|
||||
let username = payload.username.trim();
|
||||
let password = payload.password.trim();
|
||||
#[derive(Deserialize)]
|
||||
pub struct SignupStartRequest {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
if username.is_empty() || password.is_empty() {
|
||||
return Err(AppError::bad_request(
|
||||
"username and password must not be empty",
|
||||
));
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
pub struct SignupStartResponse {
|
||||
pub signup_token: String,
|
||||
pub challenge: RegistrationChallengeResponse,
|
||||
}
|
||||
|
||||
let mut conn = state.db_unscoped()?;
|
||||
|
||||
let exists: bool = dsl::users
|
||||
.filter(dsl::username.eq(username))
|
||||
.first::<User>(&mut conn)
|
||||
.optional()?
|
||||
.is_some();
|
||||
if exists {
|
||||
return Err(AppError::conflict("username already exists"));
|
||||
}
|
||||
|
||||
let user_id = Uuid::new_v4();
|
||||
let password_hash = password::hash_password(password)?;
|
||||
|
||||
insert_user(&mut conn, user_id, username, &password_hash)?;
|
||||
|
||||
let tenant_name = username.to_string();
|
||||
let tenant = state.tenants.create_tenant(
|
||||
&tenant_name,
|
||||
None,
|
||||
None,
|
||||
TenantStatus::Creating,
|
||||
&[user_id],
|
||||
Some(user_id),
|
||||
)?;
|
||||
|
||||
let user: User = dsl::users.find(user_id).first(&mut conn)?;
|
||||
|
||||
issue_session(&state, &mut conn, &user, tenant.id)
|
||||
#[derive(Deserialize)]
|
||||
pub struct SignupFinishRequest {
|
||||
pub signup_token: String,
|
||||
pub credential: RegisterPublicKeyCredential,
|
||||
pub nickname: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
@@ -150,6 +119,102 @@ pub async fn login(
|
||||
complete_login(&state, &mut conn, &user, payload.preferred_tenant_id)
|
||||
}
|
||||
|
||||
pub async fn signup_start(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SignupStartRequest>,
|
||||
) -> AppResult<Json<SignupStartResponse>> {
|
||||
let username = payload.username.trim();
|
||||
if username.is_empty() {
|
||||
return Err(AppError::bad_request("username must not be empty"));
|
||||
}
|
||||
|
||||
let mut conn = state.db_unscoped()?;
|
||||
let exists: bool = dsl::users
|
||||
.filter(dsl::username.eq(username))
|
||||
.first::<User>(&mut conn)
|
||||
.optional()?
|
||||
.is_some();
|
||||
if exists {
|
||||
return Err(AppError::conflict("username already exists"));
|
||||
}
|
||||
|
||||
let user_id = Uuid::new_v4();
|
||||
let challenge = state
|
||||
.passkeys
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::bad_request("passkey support is disabled"))?
|
||||
.start_signup_registration(&mut conn, user_id, username)?;
|
||||
|
||||
let signup_token = state
|
||||
.jwt
|
||||
.generate_signup_token(user_id, challenge.challenge_id, username.to_owned())
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
Ok(Json(SignupStartResponse {
|
||||
signup_token,
|
||||
challenge,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn signup_finish(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SignupFinishRequest>,
|
||||
) -> AppResult<Response> {
|
||||
let claims = state
|
||||
.jwt
|
||||
.verify_signup_token(&payload.signup_token)
|
||||
.map_err(|_| AppError::unauthorized())?;
|
||||
|
||||
let mut conn = state.db_unscoped()?;
|
||||
|
||||
let exists: bool = dsl::users
|
||||
.filter(dsl::username.eq(&claims.username))
|
||||
.first::<User>(&mut conn)
|
||||
.optional()?
|
||||
.is_some();
|
||||
if exists {
|
||||
return Err(AppError::conflict("username already exists"));
|
||||
}
|
||||
|
||||
let service = state
|
||||
.passkeys
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;
|
||||
|
||||
let prepared_passkey =
|
||||
service.consume_signup_challenge(&mut conn, claims.challenge_id, &payload.credential)?;
|
||||
|
||||
let state_clone = state.clone();
|
||||
let response = conn.transaction::<Response, AppError, _>(|conn| {
|
||||
let password_seed = format!("passkey-only-{}", claims.sub);
|
||||
let password_hash = password::hash_password(&password_seed)?;
|
||||
insert_user(conn, claims.sub, &claims.username, &password_hash)?;
|
||||
|
||||
let tenant = state_clone.tenants.create_tenant_with_conn(
|
||||
conn,
|
||||
&claims.username,
|
||||
None,
|
||||
None,
|
||||
TenantStatus::Creating,
|
||||
&[claims.sub],
|
||||
Some(claims.sub),
|
||||
)?;
|
||||
|
||||
let passkey_insert =
|
||||
prepared_passkey.into_new_user_passkey(claims.sub, payload.nickname.clone());
|
||||
|
||||
diesel::insert_into(passkey_dsl::user_passkeys)
|
||||
.values(&passkey_insert)
|
||||
.execute(conn)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let user: User = dsl::users.find(claims.sub).first(conn)?;
|
||||
issue_session(&state_clone, conn, &user, tenant.id)
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn refresh(
|
||||
State(state): State<AppState>,
|
||||
jar: Option<TypedHeader<Cookie>>,
|
||||
|
||||
@@ -54,7 +54,8 @@ pub fn create_router(state: AppState) -> Router<()> {
|
||||
};
|
||||
|
||||
let auth_routes = Router::new()
|
||||
.route("/signup", post(auth::signup))
|
||||
.route("/signup/start", post(auth::signup_start))
|
||||
.route("/signup/finish", post(auth::signup_finish))
|
||||
.route("/login", post(auth::login))
|
||||
.route("/refresh", post(auth::refresh))
|
||||
.route("/logout", post(auth::logout))
|
||||
|
||||
Reference in New Issue
Block a user