1use std::sync::Arc;
5
6use headless_lms_authorization::AuthorizationToken;
7use models::{
8 agent_changesets::{self, AgentChangeset, AgentChangesetCounts},
9 library::agent_changesets::{
10 self as changesets, ChangesetReview, ChangesetSummary, DiscardOutcome, PublishOptions,
11 PublishOutcome,
12 },
13};
14use utoipa::{IntoParams, OpenApi, ToSchema};
15
16use crate::{
17 domain::{
18 authorization::is_user_global_admin,
19 models_requests::{self, JwtKey},
20 request_id::RequestId,
21 },
22 prelude::*,
23};
24
25#[derive(OpenApi)]
26#[openapi(paths(
27 get_changesets,
28 get_changeset_counts,
29 get_changeset,
30 publish_changeset,
31 discard_changeset,
32 create_preview_link
33))]
34pub(crate) struct MainFrontendAgentChangesetsApiDoc;
35
36#[derive(Debug, Deserialize, IntoParams)]
37#[into_params(parameter_in = Query)]
38pub struct AgentChangesetsQuery {
39 pub course_id: Uuid,
40}
41
42#[derive(Debug, Serialize, Deserialize, ToSchema)]
44pub struct AgentPreviewLink {
45 pub preview_path: String,
47}
48
49#[derive(Debug, Serialize, Deserialize, ToSchema)]
51pub struct ReviewedRevisionRequest {
52 pub reviewed_revision: i32,
54}
55
56pub(super) async fn authorize_own_changeset(
58 conn: &mut PgConnection,
59 user_id: Uuid,
60 changeset_id: Uuid,
61) -> Result<(AgentChangeset, AuthorizationToken), ControllerError> {
62 let changeset = agent_changesets::get_by_id_for_user(conn, changeset_id, user_id).await?;
63 let token = authorize(
64 conn,
65 Act::Edit,
66 Some(user_id),
67 Res::Course(changeset.course_id),
68 )
69 .await?;
70 Ok((changeset, token))
71}
72
73#[instrument(skip(pool))]
78#[utoipa::path(
79 get,
80 path = "",
81 operation_id = "getAgentChangesets",
82 tag = "agent_changesets",
83 params(AgentChangesetsQuery),
84 responses(
85 (status = 200, description = "The user's changesets in the course", body = [ChangesetSummary])
86 )
87)]
88async fn get_changesets(
89 pool: web::Data<PgPool>,
90 query: web::Query<AgentChangesetsQuery>,
91 user: AuthUser,
92) -> ControllerResult<web::Json<Vec<ChangesetSummary>>> {
93 let mut conn = pool.acquire().await?;
94 let token = authorize(
95 &mut conn,
96 Act::Edit,
97 Some(user.id),
98 Res::Course(query.course_id),
99 )
100 .await?;
101 let summaries = changesets::get_summaries_for_user(&mut conn, user.id, query.course_id).await?;
102 token.authorized_ok(web::Json(summaries))
103}
104
105#[instrument(skip(pool))]
111#[utoipa::path(
112 get,
113 path = "/counts",
114 operation_id = "getAgentChangesetCounts",
115 tag = "agent_changesets",
116 params(AgentChangesetsQuery),
117 responses(
118 (status = 200, description = "The user's changeset counts in the course", body = AgentChangesetCounts)
119 )
120)]
121async fn get_changeset_counts(
122 pool: web::Data<PgPool>,
123 query: web::Query<AgentChangesetsQuery>,
124 user: AuthUser,
125) -> ControllerResult<web::Json<AgentChangesetCounts>> {
126 let mut conn = pool.acquire().await?;
127 let token = authorize(
128 &mut conn,
129 Act::Edit,
130 Some(user.id),
131 Res::Course(query.course_id),
132 )
133 .await?;
134 let counts = agent_changesets::get_counts(&mut conn, user.id, query.course_id).await?;
135 token.authorized_ok(web::Json(counts))
136}
137
138#[instrument(skip(pool))]
144#[utoipa::path(
145 get,
146 path = "/{changeset_id}",
147 operation_id = "getAgentChangeset",
148 tag = "agent_changesets",
149 params(
150 ("changeset_id" = Uuid, Path, description = "Changeset id")
151 ),
152 responses(
153 (status = 200, description = "The changeset with its entities and calls", body = ChangesetReview)
154 )
155)]
156async fn get_changeset(
157 pool: web::Data<PgPool>,
158 changeset_id: web::Path<Uuid>,
159 user: AuthUser,
160) -> ControllerResult<web::Json<ChangesetReview>> {
161 let mut conn = pool.acquire().await?;
162 let (changeset, token) = authorize_own_changeset(&mut conn, user.id, *changeset_id).await?;
163 let review = changesets::get_review(&mut conn, changeset).await?;
164 token.authorized_ok(web::Json(review))
165}
166
167#[instrument(skip(pool, app_conf, jwt_key))]
174#[utoipa::path(
175 post,
176 path = "/{changeset_id}/publish",
177 operation_id = "publishAgentChangeset",
178 tag = "agent_changesets",
179 params(
180 ("changeset_id" = Uuid, Path, description = "Changeset id")
181 ),
182 request_body = ReviewedRevisionRequest,
183 responses(
184 (status = 200, description = "Published, or why it was not", body = PublishOutcome)
185 )
186)]
187async fn publish_changeset(
188 request_id: RequestId,
189 pool: web::Data<PgPool>,
190 changeset_id: web::Path<Uuid>,
191 payload: web::Json<ReviewedRevisionRequest>,
192 user: AuthUser,
193 app_conf: web::Data<ApplicationConfiguration>,
194 jwt_key: web::Data<JwtKey>,
195) -> ControllerResult<web::Json<PublishOutcome>> {
196 let mut conn = pool.acquire().await?;
197 let (changeset, token) = authorize_own_changeset(&mut conn, user.id, *changeset_id).await?;
198 let can_add_global_admin_only_exercise_services =
199 is_user_global_admin(&mut conn, user.id).await?;
200 let outcome = changesets::publish(
201 &mut conn,
202 changeset.id,
203 PublishOptions {
204 can_add_global_admin_only_exercise_services,
205 reviewed_revision: Some(payload.reviewed_revision),
206 seen_revision: None,
207 },
208 &app_conf,
209 models_requests::make_spec_fetcher(
210 app_conf.base_url.clone(),
211 request_id.0,
212 Arc::clone(&jwt_key),
213 ),
214 models_requests::fetch_service_info,
215 )
216 .await?;
217 token.authorized_ok(web::Json(outcome))
218}
219
220#[instrument(skip(pool))]
226#[utoipa::path(
227 post,
228 path = "/{changeset_id}/discard",
229 operation_id = "discardAgentChangeset",
230 tag = "agent_changesets",
231 params(
232 ("changeset_id" = Uuid, Path, description = "Changeset id")
233 ),
234 request_body = ReviewedRevisionRequest,
235 responses(
236 (status = 200, description = "Discarded, or why it was not", body = DiscardOutcome)
237 )
238)]
239async fn discard_changeset(
240 pool: web::Data<PgPool>,
241 changeset_id: web::Path<Uuid>,
242 payload: web::Json<ReviewedRevisionRequest>,
243 user: AuthUser,
244) -> ControllerResult<web::Json<DiscardOutcome>> {
245 let mut conn = pool.acquire().await?;
246 let (changeset, token) = authorize_own_changeset(&mut conn, user.id, *changeset_id).await?;
247 let outcome =
248 changesets::discard(&mut conn, changeset.id, Some(payload.reviewed_revision)).await?;
249 token.authorized_ok(web::Json(outcome))
250}
251
252#[instrument(skip(pool))]
258#[utoipa::path(
259 post,
260 path = "/{changeset_id}/preview-link/{page_id}",
261 operation_id = "createAgentChangesetPreviewLink",
262 tag = "agent_changesets",
263 params(
264 ("changeset_id" = Uuid, Path, description = "Changeset id"),
265 ("page_id" = Uuid, Path, description = "Page id")
266 ),
267 responses(
268 (status = 200, description = "The preview link", body = AgentPreviewLink)
269 )
270)]
271async fn create_preview_link(
272 pool: web::Data<PgPool>,
273 path: web::Path<(Uuid, Uuid)>,
274 user: AuthUser,
275) -> ControllerResult<web::Json<AgentPreviewLink>> {
276 let (changeset_id, page_id) = path.into_inner();
277 let mut conn = pool.acquire().await?;
278 let (changeset, token) = authorize_own_changeset(&mut conn, user.id, changeset_id).await?;
279 let preview = changesets::mint_preview_token(
280 &mut conn,
281 changeset.course_id,
282 Some(changeset.id),
283 Some(page_id),
284 user.id,
285 )
286 .await?;
287 token.authorized_ok(web::Json(AgentPreviewLink {
288 preview_path: changesets::preview_path(&preview.token),
289 }))
290}
291
292pub fn _add_routes(cfg: &mut ServiceConfig) {
293 cfg.route("", web::get().to(get_changesets))
294 .route("/counts", web::get().to(get_changeset_counts))
295 .route("/{changeset_id}", web::get().to(get_changeset))
296 .route("/{changeset_id}/publish", web::post().to(publish_changeset))
297 .route("/{changeset_id}/discard", web::post().to(discard_changeset))
298 .route(
299 "/{changeset_id}/preview-link/{page_id}",
300 web::post().to(create_preview_link),
301 );
302}