Skip to content

Commit fd38395

Browse files
authored
feat: add user account approval with admin review UI (#2145)
* fix(web): use runtime CAMPSITE_API_SESSION_COOKIE for SSR session forwarding Drop NEXT_PUBLIC_CAMPSITE_API_SESSION_COOKIE from the build and entrypoint injection path so mega-ui reads the cookie name at runtime, matching Rails SESSION_COOKIE_KEY in k8s without rebuilding the image. * feat: add user account approval with admin review UI Gate new users behind Mono-backed pending/approved/rejected status, and let admins review them from settings with a top-right pending badge. * fix eslint
1 parent d049c0f commit fd38395

114 files changed

Lines changed: 1706 additions & 500 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 311 additions & 334 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ orion = { path = "orion" }
3838
orion-client = { path = "clients/orion-client" }
3939
orion-scheduler-client = { path = "clients/orion-scheduler-client" }
4040

41-
git-internal = "0.7.6"
41+
git-internal = "0.8.3"
4242
libvault-core = "0.1.0"
4343

4444
#====
@@ -81,9 +81,9 @@ sea-orm = "1.1.20"
8181
sea-orm-migration = "1.1.20"
8282

8383
#====
84-
rand = "0.10.1"
84+
rand = "0.10.2"
8585
smallvec = "1.15.2"
86-
bytes = "1.12.0"
86+
bytes = "1.12.1"
8787
chrono = { version = "0.4.45", features = ["serde"] }
8888
hex = "0.4.3"
8989
sha1 = "0.11"
@@ -95,8 +95,8 @@ idgenerator = "2.0.0"
9595
config = "0.15.25"
9696
reqwest = "0.13.4"
9797
lazy_static = "1.5.0"
98-
uuid = "1.23.4"
99-
regex = "1.12.4"
98+
uuid = "1.23.5"
99+
regex = "1.13.0"
100100
ed25519-dalek = "2.2.0"
101101
ctrlc = "3.5.2"
102102
cedar-policy = "4.11.2"
@@ -106,32 +106,33 @@ base64 = "0.22.1"
106106

107107
utoipa = { version = "5.5.0", features = ["chrono"] }
108108
utoipa-axum = "0.2.0"
109-
utoipa-swagger-ui = "9.0.2"
109+
# vendored: ship Swagger UI from crates.io so CI/airgapped builds don't curl github.com
110+
utoipa-swagger-ui = { version = "9.0.2", features = ["vendored"] }
110111
tempfile = "3.27.0"
111112
dashmap = "6.2.1"
112113
once_cell = "1.21.4"
113114
serial_test = "3.5.0"
114-
sysinfo = "0.39.5"
115+
sysinfo = "0.39.6"
115116
http = "1.4.2"
116117
url = "2.5.8"
117118
jemallocator = "0.5.4"
118119
mimalloc = "0.1.52"
119120
dotenvy = "0.15.7"
120-
tokio-tungstenite = "0.29"
121-
tungstenite = "0.29"
121+
tokio-tungstenite = "0.30"
122+
tungstenite = "0.30"
122123
itertools = "0.15.0"
123124
bs58 = "0.5.1"
124125
indexmap = "2.14"
125126
envsubst = "0.2.1"
126127
directories = "6.0.0"
127128
redis = "1.3.0"
128129
redis-test = "1.0.4"
129-
rustls = "0.23.41"
130+
rustls = "0.23.42"
130131
object_store = "0.14.0"
131132
parse-display = "0.11.0"
132133
qlean = "0.3.0"
133134
toml = "1.1.2"
134-
rkyv = "0.8.16"
135+
rkyv = "0.8.17"
135136
percent-encoding = "2.3"
136137

137138

ceres/src/application/api_service/mono/admin/group.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{
1717
fn normalize_optional_description(description: Option<String>) -> Option<String> {
1818
description
1919
.map(|item| item.trim().to_string())
20-
.and_then(|item| if item.is_empty() { None } else { Some(item) })
20+
.filter(|item| !item.is_empty())
2121
}
2222

2323
fn validate_group_name(name: &str) -> Result<String, MegaError> {

ceres/src/application/api_service/mono/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub mod stack;
2727
pub mod sync;
2828
pub mod tag;
2929
pub mod user;
30+
pub mod user_approval;
3031

3132
pub use admin::{ADMIN_FILE, EffectiveResourcePermission};
3233
pub use app_services::MonoAppServices;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//! User approval status operations for [`UserApplicationService`].
2+
3+
use callisto::user_approval_status;
4+
use common::errors::MegaError;
5+
use jupiter::storage::user_approval_storage::{
6+
APPROVAL_STATUS_APPROVED, APPROVAL_STATUS_REJECTED, UserApprovalProfile,
7+
};
8+
9+
use super::context::UserApplicationService;
10+
use crate::model::user::UserApprovalStatusRes;
11+
12+
impl UserApplicationService {
13+
pub async fn get_or_init_user_approval_status(
14+
&self,
15+
username: &str,
16+
campsite_user_id: &str,
17+
display_name: &str,
18+
email: &str,
19+
) -> Result<user_approval_status::Model, MegaError> {
20+
self.ctx
21+
.storage()
22+
.user_approval_storage()
23+
.get_or_create(
24+
username,
25+
UserApprovalProfile {
26+
campsite_user_id: campsite_user_id.to_string(),
27+
display_name: display_name.to_string(),
28+
email: email.to_string(),
29+
},
30+
)
31+
.await
32+
}
33+
34+
pub async fn list_user_approvals(
35+
&self,
36+
status: Option<&str>,
37+
limit: u64,
38+
) -> Result<Vec<user_approval_status::Model>, MegaError> {
39+
self.ctx
40+
.storage()
41+
.user_approval_storage()
42+
.list_by_status(status, limit)
43+
.await
44+
}
45+
46+
pub async fn approve_user(
47+
&self,
48+
username: &str,
49+
reviewed_by: &str,
50+
) -> Result<user_approval_status::Model, MegaError> {
51+
self.ctx
52+
.storage()
53+
.user_approval_storage()
54+
.set_status(username, APPROVAL_STATUS_APPROVED, reviewed_by)
55+
.await
56+
}
57+
58+
pub async fn reject_user(
59+
&self,
60+
username: &str,
61+
reviewed_by: &str,
62+
) -> Result<user_approval_status::Model, MegaError> {
63+
self.ctx
64+
.storage()
65+
.user_approval_storage()
66+
.set_status(username, APPROVAL_STATUS_REJECTED, reviewed_by)
67+
.await
68+
}
69+
}
70+
71+
impl From<user_approval_status::Model> for UserApprovalStatusRes {
72+
fn from(value: user_approval_status::Model) -> Self {
73+
Self {
74+
username: value.username,
75+
campsite_user_id: value.campsite_user_id,
76+
display_name: value.display_name,
77+
email: value.email,
78+
status: value.status,
79+
reviewed_by: value.reviewed_by,
80+
reviewed_at: value.reviewed_at.map(|dt| dt.and_utc().timestamp()),
81+
registered_at: value.created_at.and_utc().timestamp(),
82+
}
83+
}
84+
}

ceres/src/model/user.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use callisto::{access_token, ssh_keys};
22
use serde::{Deserialize, Serialize};
3-
use utoipa::ToSchema;
3+
use utoipa::{IntoParams, ToSchema};
44

55
#[derive(Debug, Deserialize, ToSchema)]
66
pub struct AddSSHKey {
@@ -71,3 +71,31 @@ pub struct UpdateClaContentPayload {
7171
pub struct ClaContentRes {
7272
pub content: String,
7373
}
74+
75+
#[derive(Debug, Serialize, Deserialize, ToSchema)]
76+
pub struct UserApprovalStatusRes {
77+
pub username: String,
78+
pub campsite_user_id: String,
79+
pub display_name: String,
80+
pub email: String,
81+
/// One of: pending, approved, rejected
82+
pub status: String,
83+
pub reviewed_by: Option<String>,
84+
/// Unix timestamp
85+
pub reviewed_at: Option<i64>,
86+
/// Unix timestamp
87+
pub registered_at: i64,
88+
}
89+
90+
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
91+
pub struct ListUserApprovalsQuery {
92+
/// Filter by status: pending, approved, rejected, or all (default: pending)
93+
pub status: Option<String>,
94+
/// Max rows to return (default 100, max 500)
95+
pub limit: Option<u64>,
96+
}
97+
98+
#[derive(Debug, Serialize, Deserialize, ToSchema)]
99+
pub struct UserApprovalListRes {
100+
pub items: Vec<UserApprovalStatusRes>,
101+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use sea_orm_migration::{prelude::*, schema::*};
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
impl MigrationTrait for Migration {
8+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9+
manager
10+
.create_table(
11+
Table::create()
12+
.table(UserApprovalStatus::Table)
13+
.if_not_exists()
14+
.col(
15+
ColumnDef::new(UserApprovalStatus::Username)
16+
.string()
17+
.not_null()
18+
.primary_key(),
19+
)
20+
.col(
21+
ColumnDef::new(UserApprovalStatus::CampsiteUserId)
22+
.string()
23+
.not_null()
24+
.default(""),
25+
)
26+
.col(
27+
ColumnDef::new(UserApprovalStatus::DisplayName)
28+
.string()
29+
.not_null()
30+
.default(""),
31+
)
32+
.col(
33+
ColumnDef::new(UserApprovalStatus::Email)
34+
.string()
35+
.not_null()
36+
.default(""),
37+
)
38+
.col(
39+
ColumnDef::new(UserApprovalStatus::Status)
40+
.string()
41+
.not_null()
42+
.default("pending"),
43+
)
44+
.col(string_null(UserApprovalStatus::ReviewedBy))
45+
.col(date_time_null(UserApprovalStatus::ReviewedAt))
46+
.col(date_time(UserApprovalStatus::CreatedAt))
47+
.col(date_time(UserApprovalStatus::UpdatedAt))
48+
.to_owned(),
49+
)
50+
.await?;
51+
52+
manager
53+
.create_index(
54+
Index::create()
55+
.if_not_exists()
56+
.name("idx_user_approval_status_status_created_at")
57+
.table(UserApprovalStatus::Table)
58+
.col(UserApprovalStatus::Status)
59+
.col(UserApprovalStatus::CreatedAt)
60+
.to_owned(),
61+
)
62+
.await?;
63+
64+
Ok(())
65+
}
66+
67+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
68+
manager
69+
.drop_index(
70+
Index::drop()
71+
.name("idx_user_approval_status_status_created_at")
72+
.table(UserApprovalStatus::Table)
73+
.to_owned(),
74+
)
75+
.await?;
76+
77+
manager
78+
.drop_table(Table::drop().table(UserApprovalStatus::Table).to_owned())
79+
.await
80+
}
81+
}
82+
83+
#[derive(DeriveIden)]
84+
enum UserApprovalStatus {
85+
Table,
86+
Username,
87+
CampsiteUserId,
88+
DisplayName,
89+
Email,
90+
Status,
91+
ReviewedBy,
92+
ReviewedAt,
93+
CreatedAt,
94+
UpdatedAt,
95+
}

jupiter-migrate/src/migration/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ mod m20260324_033322_fix_migration;
9898
mod m20260327_034553_drop_legacy_tasks;
9999
mod m20260413_033315_create_artifact_tables;
100100
mod m20260612_011232_drop_build_events_log;
101+
mod m20260714_021900_create_user_approval_status;
101102
mod runner;
102103
pub use runner::apply_migrations;
103104

@@ -183,6 +184,7 @@ impl MigratorTrait for Migrator {
183184
Box::new(m20260327_034553_drop_legacy_tasks::Migration),
184185
Box::new(m20260413_033315_create_artifact_tables::Migration),
185186
Box::new(m20260612_011232_drop_build_events_log::Migration),
187+
Box::new(m20260714_021900_create_user_approval_status::Migration),
186188
]
187189
}
188190
}

jupiter/callisto/src/access_token.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
1+
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
22
33
use sea_orm::entity::prelude::*;
44
use serde::{Deserialize, Serialize};

jupiter/callisto/src/artifact_objects.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
1+
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
22
33
use sea_orm::entity::prelude::*;
44
use serde::{Deserialize, Serialize};

0 commit comments

Comments
 (0)