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