diff --git a/src/main.rs b/src/main.rs index b488028..4c18179 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,13 @@ -use axum::{Json, Router, response::Html, routing::get}; +use axum::{ + Json, Router, + http::{HeaderValue, Method, header}, + response::Html, + routing::get, +}; use dotenvy::dotenv; use std::net::SocketAddr; +use tower_http::cors::AllowOrigin; +use tower_http::cors::CorsLayer; use utoipa::OpenApi; mod constants; @@ -20,6 +27,36 @@ async fn main() { .init(); let db = db::connect().await.expect("DB connection failed"); + // front end url from env + // e.g. FRONTEND_URL="http://localhost:3000,https://mydomain.com" + let env_frontend_url = + std::env::var("FRONTEND_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()); + let frontend_urls: Vec = env_frontend_url + .split(',') + .map(|s| { + s.trim() + .parse::() + .expect("invalid FRONTEND_URL") + }) + .collect(); + let cors = CorsLayer::new() + .allow_origin(AllowOrigin::list(frontend_urls)) + .allow_credentials(true) + .allow_methods([ + Method::GET, + Method::POST, + Method::PUT, + Method::PATCH, + Method::DELETE, + Method::OPTIONS, + ]) + .allow_headers([ + header::CONTENT_TYPE, + header::AUTHORIZATION, + header::ACCEPT, + header::ORIGIN, + ]); + // configure CORS let app = Router::new() .route("/api-docs/openapi.json", get(openapi_json)) @@ -30,11 +67,11 @@ async fn main() { .merge(routes::sessions::routes()) .merge(routes::email_verify::routes()) // apply simple CORS middleware (handles preflight and adds headers) - .layer(axum::middleware::from_fn(cors_middleware)) .layer(axum::middleware::from_fn_with_state( db.clone(), middleware::auth::auth_middleware, )) + .layer(cors) .with_state(db); let addr = SocketAddr::from(([0, 0, 0, 0], 8001)); @@ -77,44 +114,3 @@ async fn swagger_ui_html() -> Html<&'static str> { "#, ) } - -async fn cors_middleware( - req: axum::http::Request, - next: axum::middleware::Next, -) -> impl axum::response::IntoResponse { - use axum::http::{HeaderName, HeaderValue}; - - if req.method() == &axum::http::Method::OPTIONS { - let mut res = axum::http::Response::new(axum::body::Body::empty()); - let headers = res.headers_mut(); - headers.insert( - HeaderName::from_static("access-control-allow-origin"), - HeaderValue::from_static("*"), - ); - headers.insert( - HeaderName::from_static("access-control-allow-headers"), - HeaderValue::from_static("*"), - ); - headers.insert( - HeaderName::from_static("access-control-allow-methods"), - HeaderValue::from_static("GET,POST,PUT,DELETE,OPTIONS"), - ); - return res; - } - - let mut res = next.run(req).await; - let headers = res.headers_mut(); - headers.insert( - HeaderName::from_static("access-control-allow-origin"), - HeaderValue::from_static("*"), - ); - headers.insert( - HeaderName::from_static("access-control-allow-headers"), - HeaderValue::from_static("*"), - ); - headers.insert( - HeaderName::from_static("access-control-allow-methods"), - HeaderValue::from_static("GET,POST,PUT,DELETE,OPTIONS"), - ); - res -} diff --git a/src/models/role.rs b/src/models/role.rs index 8683837..9d873d8 100644 --- a/src/models/role.rs +++ b/src/models/role.rs @@ -32,7 +32,7 @@ impl Related for Entity { } // User -> role (many-to-many 本体) -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { super::user_role::Relation::User.def() } diff --git a/src/routes/roles.rs b/src/routes/roles.rs index b1b38c6..2389e0b 100644 --- a/src/routes/roles.rs +++ b/src/routes/roles.rs @@ -61,6 +61,8 @@ pub fn routes() -> Router { .put(put_role), ) .merge(roles_sub::search::routes()) + .merge(roles_sub::permissions::routes()) + .merge(roles_sub::users::routes()) } /// すべてのロールを取得するための関数 diff --git a/src/routes/roles_sub/mod.rs b/src/routes/roles_sub/mod.rs index a557bff..c273a8b 100644 --- a/src/routes/roles_sub/mod.rs +++ b/src/routes/roles_sub/mod.rs @@ -1 +1,3 @@ +pub mod permissions; pub mod search; +pub mod users; diff --git a/src/routes/roles_sub/permissions.rs b/src/routes/roles_sub/permissions.rs new file mode 100644 index 0000000..3d75db8 --- /dev/null +++ b/src/routes/roles_sub/permissions.rs @@ -0,0 +1,99 @@ +use crate::{ + constants::permissions::Permission, middleware::auth::AuthUser, models::user::Entity as User, + routes::users_sub::permissions::PermissionsResponse, +}; +use axum::{ + Json, Router, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + routing::*, +}; +use sea_orm::*; + +pub fn routes() -> Router { + Router::new().route("/users/{id}/permissions", get(get_permissions_bit)) +} + +/// ユーザーのロール一覧を取得し権限bitを合成する +#[utoipa::path( + get, + path = "/users/{id}/permissions", + tag = "users", + params( + ("id" = String, Path, description = "ユーザーID") + ), + responses( + (status = 200, description = "権限情報取得成功", body = PermissionsResponse), + (status = 403, description = "アクセス権限なし"), + (status = 404, description = "ユーザーが見つからない") + ), + security( + ("session_token" = []) + ) +)] +pub async fn get_permissions_bit( + State(db): State, + Path(id): Path, + auth_user: axum::Extension, +) -> Result { + // Self-only access OR read permission + if auth_user.user_id != id { + let user_roles = crate::models::user_role::Entity::find() + .filter(crate::models::user_role::Column::UserId.eq(&auth_user.user_id)) + .find_with_related(crate::models::role::Entity) + .all(&db) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let has_read_permission = user_roles.iter().any(|(_, roles)| { + roles + .iter() + .any(|r| (r.permission as i64 & Permission::USER_READ.bits() as i64) != 0) + }); + + if !has_read_permission { + return Err(StatusCode::FORBIDDEN); + } + } + + let user = User::find_by_id(id) + .one(&db) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + if let Some(user) = user { + let roles = user + .find_related(crate::models::role::Entity) + .all(&db) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut permissions_bit: i64 = 0; + for role in roles { + permissions_bit |= role.permission as i64; + } + // bitだけではなく、テキストベースでも返す + let mut permissions_text = Vec::new(); + + let perm_u32 = permissions_bit as u32; + let (mut known_names, known_mask) = Permission::names_from_bits(perm_u32); + permissions_text.append(&mut known_names); + + // 未定義のビットは PERMISSION_ でフォールバック + let remaining = perm_u32 & !known_mask; + for i in 0..32 { + if (remaining & (1u32 << i)) != 0 { + permissions_text.push(format!("PERMISSION_{}", i)); + } + } + return Ok(( + StatusCode::OK, + Json(PermissionsResponse { + permissions_bit, + permissions_text, + }), + )); + } + Err(StatusCode::NOT_FOUND) +} diff --git a/src/routes/roles_sub/users.rs b/src/routes/roles_sub/users.rs new file mode 100644 index 0000000..e983b78 --- /dev/null +++ b/src/routes/roles_sub/users.rs @@ -0,0 +1,154 @@ +use axum::{ + Json, Router, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + routing::*, +}; +use sea_orm::*; + +use crate::{ + constants::permissions::Permission, + middleware::{auth::AuthUser, permission_check}, + models::role::Entity as Role, + models::user::Entity as User, + routes::{common_dtos::array_dto::ApiResponse, roles::RoleResponse}, +}; + +pub fn routes() -> Router { + Router::new() + .route("/roles/{id}/users", get(get_all_users)) + .route("/roles/{id}/users/{uid}", delete(delete_role).put(put_role)) + //.merge(users_sub::books::routes()) +} + +/// すべてのロールを取得するための関数 +#[utoipa::path( + get, + path = "/roles/{id}/users", + tag = "roles", + params( + ("id" = String, Path, description = "ロールID") + ), + responses( + (status = 200, description = "ロールに紐つくユーザー一覧取得成功"), + (status = 404, description = "ロールが見つからない") + ), + security( + ("session_token" = []) + ) +)] +pub async fn get_all_users( + State(db): State, + Path(id): Path, + auth_user: axum::Extension, +) -> Result { + permission_check::require_permission(&auth_user, Permission::PERMISSION_MANAGE, &db).await?; + + let role_with_users = Role::find_by_id(id.clone()) + .find_with_related(User) + .all(&db) + .await + .unwrap(); + if let Some((_, users)) = role_with_users.into_iter().next() { + let responses: Vec = users + .into_iter() + .map(crate::routes::users::DetailedUserResponse::from) + .collect(); + return Ok((StatusCode::OK, Json(ApiResponse { data: responses }))); + } + Err(StatusCode::NOT_FOUND) +} + +// ユーザーにロールを付与する +#[utoipa::path( + put, + path = "/roles/{id}/users/{uid}", + tag = "roles", + params( + ("id" = String, Path, description = "ロールID"), + ("uid" = String, Path, description = "ユーザーID") + ), + responses( + (status = 201, description = "ロール付与成功", body = crate::routes::roles::RoleResponse), + (status = 403, description = "アクセス権限なし"), + (status = 404, description = "ユーザーまたはロールが見つからない") + ), + security( + ("session_token" = []) + ) +)] +pub async fn put_role( + State(db): State, + Path((id, uid)): Path<(String, String)>, + auth_user: axum::Extension, +) -> Result { + permission_check::require_permission(&auth_user, Permission::PERMISSION_MANAGE, &db).await?; + + // user と role を同時に取りに行く(並列) + let (user_res, role_res) = futures::join!( + User::find_by_id(uid.clone()).one(&db), + Role::find_by_id(id.clone()).one(&db) + ); + + match (user_res.unwrap(), role_res.unwrap()) { + (Some(user), Some(role)) => { + let user_role = crate::models::user_role::ActiveModel { + user_id: Set(user.id.clone()), + role_id: Set(role.id.clone()), + ..Default::default() + }; + // 既にある場合はエラーになるかもしれないから、必要なら重複チェックを追加 + let _ = user_role.insert(&db).await.unwrap(); + Ok((StatusCode::CREATED, Json(RoleResponse::from(role)))) + } + _ => Err(StatusCode::NOT_FOUND), + } +} + +/// ユーザーからロールを削除する +#[utoipa::path( + delete, + path = "/roles/{id}/users/{uid}", + tag = "roles", + params( + ("id" = String, Path, description = "ロールID"), + ("uid" = String, Path, description = "ユーザーID") + ), + responses( + (status = 204, description = "ロール削除成功"), + (status = 403, description = "アクセス権限なし"), + (status = 404, description = "ユーザーまたはロールが見つからない") + ), + security( + ("session_token" = []) + ) +)] +pub async fn delete_role( + State(db): State, + Path((uid, id)): Path<(String, String)>, + auth_user: axum::Extension, +) -> Result { + permission_check::require_permission(&auth_user, Permission::PERMISSION_MANAGE, &db).await?; + + // まず role の存在は確認しておくとレスポンスに role を返せる(現在の実装と同じ振る舞い) + if let Some(role) = Role::find_by_id(id.clone()).one(&db).await.unwrap() { + // 中間テーブルの該当行を直接削除 + let res = crate::models::user_role::Entity::delete_many() + .filter( + crate::models::user_role::Column::UserId + .eq(uid.clone()) + .and(crate::models::user_role::Column::RoleId.eq(id.clone())), + ) + .exec(&db) + .await + .unwrap(); + + if res.rows_affected > 0 { + return Ok((StatusCode::NO_CONTENT, Json(RoleResponse::from(role)))); + } else { + return Err(StatusCode::NOT_FOUND); + } + } + Err(StatusCode::NOT_FOUND) +} diff --git a/src/routes/users_sub/permissions.rs b/src/routes/users_sub/permissions.rs index 32079d5..cc73058 100644 --- a/src/routes/users_sub/permissions.rs +++ b/src/routes/users_sub/permissions.rs @@ -1,5 +1,7 @@ +use crate::models::role::Entity as Role; use crate::{ - constants::permissions::Permission, middleware::auth::AuthUser, models::user::Entity as User, + constants::permissions::Permission, + middleware::{auth::AuthUser, permission_check}, }; use axum::{ Json, Router, @@ -23,21 +25,21 @@ pub struct PermissionsResponse { } pub fn routes() -> Router { - Router::new().route("/users/{id}/permissions", get(get_permissions_bit)) + Router::new().route("/roles/{id}/permissions", get(get_permissions_bit)) } /// ユーザーのロール一覧を取得し権限bitを合成する #[utoipa::path( get, - path = "/users/{id}/permissions", - tag = "users", + path = "/roles/{id}/permissions", + tag = "roles", params( - ("id" = String, Path, description = "ユーザーID") + ("id" = String, Path, description = "ロールID") ), responses( (status = 200, description = "権限情報取得成功", body = PermissionsResponse), (status = 403, description = "アクセス権限なし"), - (status = 404, description = "ユーザーが見つからない") + (status = 404, description = "ロールが見つからない") ), security( ("session_token" = []) @@ -48,46 +50,34 @@ pub async fn get_permissions_bit( Path(id): Path, auth_user: axum::Extension, ) -> Result { - // Self-only access OR read permission - if auth_user.user_id != id { - let user_roles = crate::models::user_role::Entity::find() - .filter(crate::models::user_role::Column::UserId.eq(&auth_user.user_id)) - .find_with_related(crate::models::role::Entity) - .all(&db) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let has_read_permission = user_roles.iter().any(|(_, roles)| { - roles - .iter() - .any(|r| (r.permission as i64 & Permission::USER_READ.bits() as i64) != 0) - }); - - if !has_read_permission { - return Err(StatusCode::FORBIDDEN); - } + // Check if the auth_user has permission to view roles + let user_roles = crate::models::user_role::Entity::find() + .filter(crate::models::user_role::Column::UserId.eq(&auth_user.user_id)) + .find_with_related(Role) + .all(&db) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let check_permission = permission_check::get_user_permissions(&auth_user, &db) + .await? + .contains(Permission::ROLE_MANAGE); + if !check_permission + || user_roles + .iter() + .all(|(_, roles)| roles.iter().all(|role| role.id != id)) + { + return Err(StatusCode::FORBIDDEN); } - let user = User::find_by_id(id) + let role = Role::find_by_id(id) .one(&db) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - if let Some(user) = user { - let roles = user - .find_related(crate::models::role::Entity) - .all(&db) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut permissions_bit: i64 = 0; - for role in roles { - permissions_bit |= role.permission as i64; - } + if let Some(role) = role { // bitだけではなく、テキストベースでも返す let mut permissions_text = Vec::new(); - let perm_u32 = permissions_bit as u32; + let perm_u32 = role.permission as u32; let (mut known_names, known_mask) = Permission::names_from_bits(perm_u32); permissions_text.append(&mut known_names); @@ -101,7 +91,7 @@ pub async fn get_permissions_bit( return Ok(( StatusCode::OK, Json(PermissionsResponse { - permissions_bit, + permissions_bit: role.permission as i64, permissions_text, }), ));