headless_lms_server/programs/seed/
seed_oauth_clients.rs

1use std::str::FromStr;
2
3use headless_lms_models::{
4    library::oauth::{Digest, GrantTypeName, pkce},
5    oauth_client,
6};
7use sqlx::{Pool, Postgres};
8use uuid::Uuid;
9
10pub struct SeedOAuthClientsResult {
11    pub client_db_id: Uuid,
12}
13
14const TEST_CLIENT_IDS: &[&str] = &["test-client-id", "test-client-id-2", "test-client-id-3"];
15
16pub async fn seed_oauth_clients(db_pool: Pool<Postgres>) -> anyhow::Result<SeedOAuthClientsResult> {
17    info!("Inserting OAuth Clients");
18    let secret =
19        Digest::from_str("396b544a35b29f7d613452a165dcaebf4d71b80e981e687e91ce6d9ba9679cb2")
20            .unwrap(); // "very-secret"
21    let mut conn = db_pool.acquire().await?;
22    // One redirect URI per Playwright worker (ports 8765..8784) so each worker has its own callback server.
23    // Must match system-tests getRedirectUri(): http://127.0.0.1:{port}/callback
24    let mut redirect_uris: Vec<String> = (8765..=8784)
25        .map(|p| format!("http://127.0.0.1:{p}/callback"))
26        .collect();
27    redirect_uris.push("https://localhost.emobix.co.uk:8443/test/a/testing/callback".to_string());
28
29    // Update redirect_uris for existing test clients so re-running seed fixes "redirect_uri does not match client".
30    for client_id in TEST_CLIENT_IDS {
31        let updated = sqlx::query(
32            "UPDATE oauth_clients SET redirect_uris = $1, updated_at = now() WHERE client_id = $2 AND deleted_at IS NULL",
33        )
34        .bind(&redirect_uris)
35        .bind(*client_id)
36        .execute(&mut *conn)
37        .await?;
38        if updated.rows_affected() > 0 {
39            info!(
40                "Updated redirect_uris for existing OAuth client {}",
41                client_id
42            );
43        }
44    }
45
46    let scopes = vec![
47        "openid".to_string(),
48        "profile".to_string(),
49        "email".to_string(),
50        "offline_access".to_string(),
51    ];
52    let allowed_grant_types = vec![
53        GrantTypeName::AuthorizationCode,
54        GrantTypeName::RefreshToken,
55    ];
56    let pkce_methods_allowed = vec![pkce::PkceMethod::S256];
57
58    let new_client_parms = oauth_client::NewClientParams {
59        client_name: "Test Client",
60        application_type: oauth_client::ApplicationType::Web,
61        client_id: "test-client-id",
62        client_secret: Some(&secret), // "very-secret"
63        client_secret_expires_at: None,
64        redirect_uris: redirect_uris.as_slice(),
65        allowed_grant_types: &allowed_grant_types,
66        scopes: scopes.as_slice(),
67        origin: "http://localhost",
68        bearer_allowed: true,
69        pkce_methods_allowed: &pkce_methods_allowed,
70        post_logout_redirect_uris: None,
71        require_pkce: true,
72        token_endpoint_auth_method: oauth_client::TokenEndpointAuthMethod::ClientSecretPost,
73    };
74
75    let client = if let Some(existing) =
76        oauth_client::OAuthClient::find_by_client_id_optional(&mut conn, "test-client-id").await?
77    {
78        existing
79    } else {
80        oauth_client::OAuthClient::insert(&mut conn, new_client_parms).await?
81    };
82
83    let new_client_parms_2 = oauth_client::NewClientParams {
84        client_name: "Test Client 2",
85        application_type: oauth_client::ApplicationType::Web,
86        client_id: "test-client-id-2",
87        client_secret: Some(&secret), // "very-secret"
88        client_secret_expires_at: None,
89        redirect_uris: redirect_uris.as_slice(),
90        allowed_grant_types: &allowed_grant_types,
91        scopes: scopes.as_slice(),
92        origin: "http://localhost",
93        bearer_allowed: true,
94        pkce_methods_allowed: &pkce_methods_allowed,
95        post_logout_redirect_uris: None,
96        require_pkce: false,
97        token_endpoint_auth_method: oauth_client::TokenEndpointAuthMethod::ClientSecretPost,
98    };
99    if oauth_client::OAuthClient::find_by_client_id_optional(&mut conn, "test-client-id-2")
100        .await?
101        .is_none()
102    {
103        let _client_2 = oauth_client::OAuthClient::insert(&mut conn, new_client_parms_2).await?;
104    }
105
106    let new_client_parms_3 = oauth_client::NewClientParams {
107        client_name: "Test Client 3",
108        application_type: oauth_client::ApplicationType::Web,
109        client_id: "test-client-id-3",
110        client_secret: Some(&secret), // "very-secret"
111        client_secret_expires_at: None,
112        redirect_uris: redirect_uris.as_slice(),
113        allowed_grant_types: &allowed_grant_types,
114        scopes: scopes.as_slice(),
115        origin: "http://localhost",
116        bearer_allowed: true,
117        pkce_methods_allowed: &pkce_methods_allowed,
118        post_logout_redirect_uris: None,
119        require_pkce: false,
120        token_endpoint_auth_method: oauth_client::TokenEndpointAuthMethod::ClientSecretPost,
121    };
122    if oauth_client::OAuthClient::find_by_client_id_optional(&mut conn, "test-client-id-3")
123        .await?
124        .is_none()
125    {
126        let _client_3 = oauth_client::OAuthClient::insert(&mut conn, new_client_parms_3).await?;
127    }
128
129    Ok(SeedOAuthClientsResult {
130        client_db_id: client.id,
131    })
132}