headless_lms_server/controllers/main_frontend/credit_registration_admin/
student_numbers.rs1use headless_lms_models::credit_registration_admin_actions::{
4 CreditRegistrationAdminAction, CreditRegistrationAdminActionTarget, GLOBAL_ADMIN_ROLE,
5 NewCreditRegistrationAdminAction,
6};
7use headless_lms_models::credit_registration_events::CreditRegistrationEventKind;
8use headless_lms_models::library::credit_registration::student_number_change::unlink_verified_student_number;
9use headless_lms_models::verified_student_numbers::{
10 self, AdminVerifiedStudentNumber, StudentNumberVerificationMethod,
11};
12use utoipa::ToSchema;
13
14use crate::prelude::*;
15use headless_lms_utils::secret_string::expose_option;
16use secrecy::{ExposeSecret, SecretString};
17
18use super::{authorize_credit_registration_admin, required_reason};
19
20#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
21pub struct AdminVerifiedStudentNumberRow {
22 pub id: Uuid,
23 pub user_id: Uuid,
24 pub user_email: Option<String>,
26 pub first_name: Option<String>,
27 pub last_name: Option<String>,
28 pub student_number: String,
29 pub sisu_person_id: Option<String>,
31 pub verified_at: DateTime<Utc>,
32 pub verified_via: StudentNumberVerificationMethod,
33 pub verified_via_email: Option<String>,
35 pub linked_by_user_id: Option<Uuid>,
36 pub link_reason: Option<String>,
37 pub verified_from_course_id: Option<Uuid>,
38 pub live_registration_count: i64,
39}
40
41#[derive(Debug, Deserialize, ToSchema)]
42pub struct AdminUnlinkStudentNumberPayload {
43 pub reason: String,
44}
45
46#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
47pub struct AdminUnlinkStudentNumberResult {
48 pub affected_registration_count: i64,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct ListVerifiedStudentNumbersQuery {
54 page: Option<u32>,
55 limit: Option<u32>,
56 verified_via: Option<StudentNumberVerificationMethod>,
57 search: Option<SecretString>,
58}
59
60#[instrument(skip(pool))]
65#[utoipa::path(
66 get,
67 path = "/student-numbers",
68 operation_id = "listVerifiedStudentNumbersForAdmin",
69 tag = "credit-registration-admin",
70 params(
71 ("page" = Option<u32>, Query, description = "Page number, from 1"),
72 ("limit" = Option<u32>, Query, description = "Rows per page"),
73 ("verified_via" = Option<StudentNumberVerificationMethod>, Query, description = "How the link was established"),
74 ("search" = Option<String>, Query, description = "Student number, name or email")
75 ),
76 responses(
77 (status = 200, description = "A page of the live links", body = Page<AdminVerifiedStudentNumberRow>)
78 )
79)]
80pub async fn list_verified_student_numbers_for_admin(
81 user: AuthUser,
82 pool: web::Data<PgPool>,
83 query: web::Query<ListVerifiedStudentNumbersQuery>,
84) -> ControllerResult<web::Json<Page<AdminVerifiedStudentNumberRow>>> {
85 let mut conn = pool.acquire().await?;
86 let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
87
88 let pagination = parse_pagination(query.page, query.limit, 50)?;
89 let (rows, total_count) = verified_student_numbers::get_admin_page(
90 &mut conn,
91 query.verified_via,
92 expose_option(&query.search),
93 pagination.limit(),
94 pagination.offset(),
95 )
96 .await?;
97 let data = rows.into_iter().map(to_admin_student_number).collect();
98
99 token.authorized_ok(web::Json(Page::new(pagination, data, total_count)))
100}
101
102#[instrument(skip(pool, payload))]
109#[utoipa::path(
110 post,
111 path = "/student-numbers/{verified_student_number_id}/unlink",
112 operation_id = "adminUnlinkStudentNumber",
113 tag = "credit-registration-admin",
114 params(("verified_student_number_id" = Uuid, Path, description = "Verified student number id")),
115 request_body = AdminUnlinkStudentNumberPayload,
116 responses(
117 (status = 200, description = "How many registrations went back to waiting", body = AdminUnlinkStudentNumberResult),
118 (status = 422, description = "No reason given"),
119 (status = 404, description = "No such link")
120 )
121)]
122pub async fn admin_unlink_student_number(
123 user: AuthUser,
124 pool: web::Data<PgPool>,
125 verified_student_number_id: web::Path<Uuid>,
126 payload: web::Json<AdminUnlinkStudentNumberPayload>,
127) -> ControllerResult<web::Json<AdminUnlinkStudentNumberResult>> {
128 let mut conn = pool.acquire().await?;
129 let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
130
131 let reason = required_reason(&payload.reason)?;
132 let id = *verified_student_number_id;
133 let link = verified_student_numbers::get_by_id(&mut conn, id).await?;
134
135 let mut tx = conn.begin().await?;
136 let affected_registration_count = unlink_verified_student_number(
137 &mut tx,
138 id,
139 link.user_id,
140 Some(user.id),
141 CreditRegistrationEventKind::AdminAction,
142 "An administrator unlinked this student number.",
143 )
144 .await?;
145 models::credit_registration_admin_actions::record(
146 &mut tx,
147 &NewCreditRegistrationAdminAction {
148 target_id: Some(id),
149 reason: Some(reason.to_string()),
150 details: Some(serde_json::json!({
151 "user_id": link.user_id,
152 "student_number": link.student_number.expose_secret(),
153 "verified_via": link.verified_via,
154 })),
155 affected_row_count: Some(
156 i32::try_from(affected_registration_count).unwrap_or(i32::MAX),
157 ),
158 ..NewCreditRegistrationAdminAction::new(
159 CreditRegistrationAdminAction::UnlinkStudentNumber,
160 CreditRegistrationAdminActionTarget::VerifiedStudentNumber,
161 user.id,
162 GLOBAL_ADMIN_ROLE,
163 )
164 },
165 )
166 .await?;
167 tx.commit().await?;
168
169 token.authorized_ok(web::Json(AdminUnlinkStudentNumberResult {
170 affected_registration_count,
171 }))
172}
173
174fn to_admin_student_number(row: AdminVerifiedStudentNumber) -> AdminVerifiedStudentNumberRow {
175 AdminVerifiedStudentNumberRow {
176 id: row.id,
177 user_id: row.user_id,
178 user_email: row.user_email,
179 first_name: row.first_name,
180 last_name: row.last_name,
181 student_number: row.student_number.expose_secret().to_owned(),
182 sisu_person_id: expose_option(&row.sisu_person_id).map(str::to_owned),
183 verified_at: row.verified_at,
184 verified_via: row.verified_via,
185 verified_via_email: expose_option(&row.verified_via_email).map(str::to_owned),
186 linked_by_user_id: row.linked_by_user_id,
187 link_reason: row.link_reason,
188 verified_from_course_id: row.verified_from_course_id,
189 live_registration_count: row.live_registration_count,
190 }
191}
192
193pub fn _add_routes(cfg: &mut ServiceConfig) {
194 cfg.route(
195 "/student-numbers",
196 web::get().to(list_verified_student_numbers_for_admin),
197 )
198 .route(
199 "/student-numbers/{verified_student_number_id}/unlink",
200 web::post().to(admin_unlink_student_number),
201 );
202}