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};
13
14#[instrument(skip(pool))]
34pub async fn approve_consent(
35 pool: web::Data<PgPool>,
36 form: web::Json<ConsentQuery>,
37 user: AuthUser,
38) -> ControllerResult<HttpResponse> {
39 let mut conn = pool.acquire().await?;
40 let token = skip_authorize();
41
42 let client = OAuthClient::find_by_client_id(&mut conn, &form.client_id).await?;
43 if !client.redirect_uris.contains(&form.redirect_uri) {
44 return Err(oauth_invalid_request(
45 "redirect_uri does not match client",
46 None, Some(&form.state),
48 ));
49 }
50
51 let requested_scopes: Vec<String> = form
53 .scope
54 .split_whitespace()
55 .map(|s| s.to_string())
56 .collect();
57
58 let allowed_scopes = &client.scopes;
59
60 for scope in &requested_scopes {
61 if !allowed_scopes.contains(scope) {
62 return Err(oauth_invalid_request(
63 "invalid scope",
64 None,
65 Some(&form.state),
66 ));
67 }
68 }
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]
119pub async fn deny_consent(
120 pool: web::Data<PgPool>,
121 form: web::Json<ConsentDenyQuery>,
122) -> Result<HttpResponse, Error> {
123 let mut conn = pool.acquire().await.map_err(|e| {
124 tracing::error!(err = %e, "OAuth consent/deny: pool acquire failed");
125 actix_web::error::ErrorInternalServerError(e)
126 })?;
127
128 let client_result = OAuthClient::find_by_client_id(&mut conn, &form.client_id).await;
129 let client = match client_result {
130 Ok(c) => c,
131 Err(err) => {
132 return Err(match err.error_type() {
133 ModelErrorType::RecordNotFound | ModelErrorType::NotFound => {
134 actix_web::error::ErrorNotFound("client not found")
135 }
136 _ => {
137 tracing::error!(err = %err, "OAuth consent/deny: client lookup failed");
138 actix_web::error::ErrorInternalServerError(err)
139 }
140 });
141 }
142 };
143
144 if !client.redirect_uris.contains(&form.redirect_uri) {
145 return Err(actix_web::error::ErrorBadRequest("invalid redirect URI"));
146 }
147
148 let mut url = Url::parse(&form.redirect_uri)
149 .map_err(|_| actix_web::error::ErrorBadRequest("invalid redirect URI"))?;
150
151 {
152 let mut qp = url.query_pairs_mut();
153 qp.append_pair("error", "access_denied");
154 if !form.state.is_empty() {
155 qp.append_pair("state", &form.state);
156 }
157 }
158
159 Ok(HttpResponse::Found()
160 .append_header(("Location", url.to_string()))
161 .finish())
162}
163
164pub fn _add_routes(cfg: &mut web::ServiceConfig) {
165 cfg.route("/consent", web::post().to(approve_consent))
166 .route("/consent/deny", web::post().to(deny_consent));
167}