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;
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: Vec<String> = form
69 .scope
70 .split_whitespace()
71 .map(|s| s.to_string())
72 .collect();
73
74 let allowed_scopes = &client.scopes;
75
76 for scope in &requested_scopes {
77 if !allowed_scopes.contains(scope) {
78 return Err(oauth_invalid_request(
79 "invalid scope",
80 None,
81 Some(&form.state),
82 ));
83 }
84 }
85
86 OAuthUserClientScopes::insert(&mut conn, user.id, client.id, &requested_scopes).await?;
87
88 let mut query_string = String::new();
90 {
91 let mut query_builder = form_urlencoded::Serializer::new(&mut query_string);
92 query_builder
93 .append_pair("client_id", &form.client_id)
94 .append_pair("redirect_uri", &form.redirect_uri)
95 .append_pair("scope", &form.scope)
96 .append_pair("state", &form.state)
97 .append_pair("nonce", &form.nonce)
98 .append_pair("response_type", &form.response_type);
99
100 if let Some(code_challenge) = &form.code_challenge {
102 query_builder.append_pair("code_challenge", code_challenge);
103 }
104 if let Some(code_challenge_method) = &form.code_challenge_method {
105 query_builder.append_pair("code_challenge_method", code_challenge_method);
106 }
107 }
108 let query = query_string;
109
110 let location = format!("/api/v0/main-frontend/oauth/authorize?{}", query);
112
113 token.authorized_ok(HttpResponse::Ok().json(ConsentResponse {
114 redirect_uri: location,
115 }))
116}
117
118#[instrument]
135#[utoipa::path(
136 post,
137 path = "/consent/deny",
138 operation_id = "denyOauthConsent",
139 tag = "oauth",
140 request_body = ConsentDenyQuery,
141 responses(
142 (status = 200, description = "Consent denial response", body = ConsentResponse)
143 )
144)]
145pub async fn deny_consent(
146 pool: web::Data<PgPool>,
147 form: web::Json<ConsentDenyQuery>,
148) -> Result<HttpResponse, Error> {
149 let mut conn = pool.acquire().await.map_err(|e| {
150 tracing::error!(err = %e, "OAuth consent/deny: pool acquire failed");
151 actix_web::error::ErrorInternalServerError(e)
152 })?;
153
154 let client_result = OAuthClient::find_by_client_id(&mut conn, &form.client_id).await;
155 let client = match client_result {
156 Ok(c) => c,
157 Err(err) => {
158 return Err(match err.error_type() {
159 ModelErrorType::RecordNotFound | ModelErrorType::NotFound => {
160 actix_web::error::ErrorNotFound("client not found")
161 }
162 _ => {
163 tracing::error!(err = %err, "OAuth consent/deny: client lookup failed");
164 actix_web::error::ErrorInternalServerError(err)
165 }
166 });
167 }
168 };
169
170 if !client.redirect_uris.contains(&form.redirect_uri) {
171 return Err(actix_web::error::ErrorBadRequest("invalid redirect URI"));
172 }
173
174 let mut url = Url::parse(&form.redirect_uri)
175 .map_err(|_| actix_web::error::ErrorBadRequest("invalid redirect URI"))?;
176
177 {
178 let mut qp = url.query_pairs_mut();
179 qp.append_pair("error", "access_denied");
180 if !form.state.is_empty() {
181 qp.append_pair("state", &form.state);
182 }
183 }
184
185 Ok(HttpResponse::Ok().json(ConsentResponse {
186 redirect_uri: url.to_string(),
187 }))
188}
189
190pub fn _add_routes(cfg: &mut web::ServiceConfig) {
191 cfg.route("/consent", web::post().to(approve_consent))
192 .route("/consent/deny", web::post().to(deny_consent));
193}