headless_lms_server/controllers/main_frontend/oauth/
consent.rs1use crate::domain::oauth::consent_deny_query::ConsentDenyQuery;
2use crate::domain::oauth::consent_query::ConsentQuery;
3use crate::domain::oauth::consent_response::ConsentResponse;
4use crate::domain::oauth::helpers::{oauth_invalid_request, split_and_validate_scopes};
5use crate::prelude::*;
6use actix_web::{Error, HttpResponse, web};
7use models::{
8 error::ModelErrorType, oauth_client::OAuthClient,
9 oauth_user_client_scopes::OAuthUserClientScopes,
10};
11use sqlx::PgPool;
12use url::{Url, form_urlencoded};
13use utoipa::OpenApi;
14
15#[derive(OpenApi)]
16#[openapi(paths(approve_consent, deny_consent))]
17#[allow(dead_code)]
18pub(crate) struct MainFrontendOauthConsentApiDoc;
19
20#[instrument(skip(pool))]
40#[utoipa::path(
41 post,
42 path = "/consent",
43 operation_id = "approveOauthConsent",
44 tag = "oauth",
45 request_body = ConsentQuery,
46 responses(
47 (status = 200, description = "Consent approval response", body = ConsentResponse)
48 )
49)]
50pub async fn approve_consent(
51 pool: web::Data<PgPool>,
52 form: web::Json<ConsentQuery>,
53 user: AuthUser,
54) -> ControllerResult<HttpResponse> {
55 let mut conn = pool.acquire().await?;
56 let token = skip_authorize();
57
58 let client = OAuthClient::find_by_client_id(&mut conn, &form.client_id).await?;
59 if !client.redirect_uris.contains(&form.redirect_uri) {
60 return Err(oauth_invalid_request(
61 "redirect_uri does not match client",
62 None, Some(&form.state),
64 ));
65 }
66
67 let requested_scopes = split_and_validate_scopes(&form.scope, &client.scopes)
68 .map_err(|_| oauth_invalid_request("invalid scope", None, Some(&form.state)))?;
69
70 OAuthUserClientScopes::insert(&mut conn, user.id, client.id, &requested_scopes).await?;
71
72 let mut query_string = String::new();
74 {
75 let mut query_builder = form_urlencoded::Serializer::new(&mut query_string);
76 query_builder
77 .append_pair("client_id", &form.client_id)
78 .append_pair("redirect_uri", &form.redirect_uri)
79 .append_pair("scope", &form.scope)
80 .append_pair("state", &form.state)
81 .append_pair("nonce", &form.nonce)
82 .append_pair("response_type", &form.response_type);
83
84 if let Some(code_challenge) = &form.code_challenge {
86 query_builder.append_pair("code_challenge", code_challenge);
87 }
88 if let Some(code_challenge_method) = &form.code_challenge_method {
89 query_builder.append_pair("code_challenge_method", code_challenge_method);
90 }
91 }
92 let query = query_string;
93
94 let location = format!("/api/v0/main-frontend/oauth/authorize?{}", query);
96
97 token.authorized_ok(HttpResponse::Ok().json(ConsentResponse {
98 redirect_uri: location,
99 }))
100}
101
102#[instrument]
119#[utoipa::path(
120 post,
121 path = "/consent/deny",
122 operation_id = "denyOauthConsent",
123 tag = "oauth",
124 request_body = ConsentDenyQuery,
125 responses(
126 (status = 200, description = "Consent denial response", body = ConsentResponse)
127 )
128)]
129pub async fn deny_consent(
130 pool: web::Data<PgPool>,
131 form: web::Json<ConsentDenyQuery>,
132) -> Result<HttpResponse, Error> {
133 let mut conn = pool.acquire().await.map_err(|e| {
134 tracing::error!(err = %e, "OAuth consent/deny: pool acquire failed");
135 actix_web::error::ErrorInternalServerError(e)
136 })?;
137
138 let client_result = OAuthClient::find_by_client_id(&mut conn, &form.client_id).await;
139 let client = match client_result {
140 Ok(c) => c,
141 Err(err) => {
142 return Err(match err.error_type() {
143 ModelErrorType::RecordNotFound | ModelErrorType::NotFound => {
144 actix_web::error::ErrorNotFound("client not found")
145 }
146 _ => {
147 tracing::error!(err = %err, "OAuth consent/deny: client lookup failed");
148 actix_web::error::ErrorInternalServerError(err)
149 }
150 });
151 }
152 };
153
154 if !client.redirect_uris.contains(&form.redirect_uri) {
155 return Err(actix_web::error::ErrorBadRequest("invalid redirect URI"));
156 }
157
158 let mut url = Url::parse(&form.redirect_uri)
159 .map_err(|_| actix_web::error::ErrorBadRequest("invalid redirect URI"))?;
160
161 {
162 let mut qp = url.query_pairs_mut();
163 qp.append_pair("error", "access_denied");
164 if !form.state.is_empty() {
165 qp.append_pair("state", &form.state);
166 }
167 }
168
169 Ok(HttpResponse::Ok().json(ConsentResponse {
170 redirect_uri: url.to_string(),
171 }))
172}
173
174pub fn _add_routes(cfg: &mut web::ServiceConfig) {
175 cfg.route("/consent", web::post().to(approve_consent))
176 .route("/consent/deny", web::post().to(deny_consent));
177}