Skip to main content

headless_lms_server/controllers/main_frontend/
shared_submissions.rs

1use crate::{domain::models_requests, prelude::*};
2use headless_lms_models::exercise_slide_submission_shares::ExerciseSlideSubmissionShare;
3use headless_lms_models::exercise_slide_submissions::ExerciseSlideSubmissionInfo;
4use utoipa::OpenApi;
5
6#[derive(OpenApi)]
7#[openapi(paths(
8    get_shared_submission_info,
9    list_own_shares,
10    revoke_share,
11    revoke_shares_of_submission
12))]
13pub(crate) struct MainFrontendSharedSubmissionsApiDoc;
14
15/**
16GET `/api/v0/main-frontend/shared-submissions/{token}` - Returns the data needed to
17render a shared submission.
18
19The `token` is the unguessable share id minted by the client share endpoint. Login is
20required, but holding the token is the only capability needed to view the submission —
21no teacher or course role.
22*/
23#[utoipa::path(
24    get,
25    path = "/{token}",
26    operation_id = "getSharedSubmissionInfo",
27    tag = "shared_submissions",
28    params(
29        ("token" = Uuid, Path, description = "Submission share token")
30    ),
31    responses(
32        (status = 200, description = "Data needed to render the shared submission", body = ExerciseSlideSubmissionInfo)
33    )
34)]
35#[instrument(skip(pool, file_store, app_conf))]
36async fn get_shared_submission_info(
37    token: web::Path<Uuid>,
38    pool: web::Data<PgPool>,
39    _user: AuthUser,
40    file_store: web::Data<dyn FileStore>,
41    app_conf: web::Data<ApplicationConfiguration>,
42) -> ControllerResult<web::Json<ExerciseSlideSubmissionInfo>> {
43    let mut conn = pool.acquire().await?;
44    // Possession of the share token is the capability; any logged-in user may view it.
45    let auth_token = skip_authorize();
46
47    let share = models::exercise_slide_submission_shares::get_by_id(&mut conn, *token).await?;
48    let submission = models::exercise_slide_submissions::get_by_id(
49        &mut conn,
50        share.exercise_slide_submission_id,
51    )
52    .await?;
53    let mut res = models::exercise_slide_submissions::get_exercise_slide_submission_info(
54        &mut conn,
55        share.exercise_slide_submission_id,
56        submission.user_id,
57        models_requests::fetch_service_info,
58        true,
59        file_store.as_ref(),
60        app_conf.as_ref(),
61    )
62    .await?;
63
64    // A forwardable share link must never leak the model solution or the submitter's
65    // user id; see `strip_for_shared_view`.
66    res.strip_for_shared_view();
67
68    auth_token.authorized_ok(web::Json(res))
69}
70
71/**
72GET `/api/v0/main-frontend/shared-submissions` - Lists the shares the current user has minted,
73newest first, so they can be reviewed and withdrawn.
74*/
75#[utoipa::path(
76    get,
77    path = "",
78    operation_id = "listOwnSubmissionShares",
79    tag = "shared_submissions",
80    responses(
81        (status = 200, description = "The caller's live shares, newest first", body = Vec<ExerciseSlideSubmissionShare>)
82    )
83)]
84#[instrument(skip(pool))]
85async fn list_own_shares(
86    pool: web::Data<PgPool>,
87    user: AuthUser,
88) -> ControllerResult<web::Json<Vec<ExerciseSlideSubmissionShare>>> {
89    let mut conn = pool.acquire().await?;
90    // Scoped to the caller's own shares, so no further authorization is needed.
91    let auth_token = skip_authorize();
92    let shares =
93        models::exercise_slide_submission_shares::list_by_creator(&mut conn, user.id).await?;
94    auth_token.authorized_ok(web::Json(shares))
95}
96
97/**
98DELETE `/api/v0/main-frontend/shared-submissions/{token}` - Withdraws one share, after which the
99link stops resolving.
100
101Only the share's creator may revoke it: holding the token is authority to view, not to withdraw.
102Revoking an unknown or already-revoked share is a no-op, reported as `false`.
103*/
104#[utoipa::path(
105    delete,
106    path = "/{token}",
107    operation_id = "revokeSubmissionShare",
108    tag = "shared_submissions",
109    params(
110        ("token" = Uuid, Path, description = "Submission share token")
111    ),
112    responses(
113        (status = 200, description = "Whether a live share was withdrawn", body = bool)
114    )
115)]
116#[instrument(skip(pool))]
117async fn revoke_share(
118    token: web::Path<Uuid>,
119    pool: web::Data<PgPool>,
120    user: AuthUser,
121) -> ControllerResult<web::Json<bool>> {
122    let mut conn = pool.acquire().await?;
123    // The `created_by` filter in the query is the authorization check.
124    let auth_token = skip_authorize();
125    let revoked =
126        models::exercise_slide_submission_shares::revoke(&mut conn, *token, user.id).await?;
127    auth_token.authorized_ok(web::Json(revoked))
128}
129
130/**
131DELETE `/api/v0/main-frontend/shared-submissions/of-submission/{submission_id}` - Withdraws every
132share the current user has minted for one submission.
133*/
134#[utoipa::path(
135    delete,
136    path = "/of-submission/{submission_id}",
137    operation_id = "revokeSubmissionSharesOfSubmission",
138    tag = "shared_submissions",
139    params(
140        ("submission_id" = Uuid, Path, description = "Exercise slide submission id")
141    ),
142    responses(
143        (status = 200, description = "How many shares were withdrawn", body = i64)
144    )
145)]
146#[instrument(skip(pool))]
147async fn revoke_shares_of_submission(
148    submission_id: web::Path<Uuid>,
149    pool: web::Data<PgPool>,
150    user: AuthUser,
151) -> ControllerResult<web::Json<i64>> {
152    let mut conn = pool.acquire().await?;
153    // The `created_by` filter in the query is the authorization check.
154    let auth_token = skip_authorize();
155    let revoked = models::exercise_slide_submission_shares::revoke_all_for_submission(
156        &mut conn,
157        *submission_id,
158        user.id,
159    )
160    .await?;
161    auth_token.authorized_ok(web::Json(revoked as i64))
162}
163
164pub fn _add_routes(cfg: &mut ServiceConfig) {
165    cfg.route("", web::get().to(list_own_shares))
166        .route(
167            "/of-submission/{submission_id}",
168            web::delete().to(revoke_shares_of_submission),
169        )
170        .route("/{token}", web::get().to(get_shared_submission_info))
171        .route("/{token}", web::delete().to(revoke_share));
172}