squigit-auth 0.1.0

OAuth profiles and encrypted provider credentials for Squigit
Documentation
// Copyright 2026 a7mddra
// SPDX-License-Identifier: Apache-2.0

use std::fs;
use std::sync::Once;

use serde::Deserialize;

use crate::{ProfileError, Result};

use super::AuthFlowSettings;

static AUTH_MISSING_CREDENTIALS_LOG_ONCE: Once = Once::new();

#[derive(Clone, Debug)]
pub enum CredentialsSource {
    Auto,
    RawJson(String),
}

#[derive(Deserialize, Debug)]
struct GoogleCredentials {
    installed: Option<OAuthConfig>,
    web: Option<OAuthConfig>,
}

#[derive(Deserialize, Debug, Clone)]
pub(super) struct OAuthConfig {
    pub(super) client_id: String,
    #[serde(rename = "client_secret")]
    pub(super) client_secret: Option<String>,
    pub(super) auth_uri: String,
    pub(super) token_uri: String,
}

fn missing_credentials_message() -> String {
    "Google authentication credentials were not provided. In release mode, credentials must be supplied explicitly (e.g. via squigit-rs runtime secrets, CredentialsSource::RawJson, or SQUIGIT_GOOGLE_CREDENTIALS_JSON / SQUIGIT_GOOGLE_CREDENTIALS_PATH).".to_string()
}

fn load_google_credentials_raw(source: &CredentialsSource) -> Result<String> {
    match source {
        CredentialsSource::RawJson(raw) => Ok(raw.clone()),
        CredentialsSource::Auto => {
            if let Ok(raw) = std::env::var("SQUIGIT_GOOGLE_CREDENTIALS_JSON") {
                if !raw.trim().is_empty() {
                    return Ok(raw);
                }
            }

            if let Ok(path) = std::env::var("SQUIGIT_GOOGLE_CREDENTIALS_PATH") {
                let trimmed = path.trim();
                if !trimmed.is_empty() {
                    return fs::read_to_string(trimmed).map_err(|err| {
                        ProfileError::Auth(format!(
                            "Failed reading SQUIGIT_GOOGLE_CREDENTIALS_PATH: {}",
                            err
                        ))
                    });
                }
            }

            let message = missing_credentials_message();
            AUTH_MISSING_CREDENTIALS_LOG_ONCE.call_once(|| {
                eprintln!("[auth] {}", message.replace('\n', "\n[auth] "));
            });
            Err(ProfileError::MissingCredentials(message))
        }
    }
}

fn is_placeholder_config(config: &OAuthConfig) -> bool {
    config.client_id.contains("replace-me") || config.client_id.trim().is_empty()
}

pub(super) fn load_google_oauth_config(settings: &AuthFlowSettings) -> Result<OAuthConfig> {
    let raw = load_google_credentials_raw(&settings.credentials_source)?;
    let raw = raw.trim();
    if raw.is_empty() {
        let message = missing_credentials_message();
        AUTH_MISSING_CREDENTIALS_LOG_ONCE.call_once(|| {
            eprintln!("[auth] {}", message.replace('\n', "\n[auth] "));
        });
        return Err(ProfileError::MissingCredentials(message));
    }

    let wrapper: GoogleCredentials = serde_json::from_str(raw).map_err(|err| {
        ProfileError::Auth(format!("Failed to parse Google OAuth credentials: {}", err))
    })?;

    let config = wrapper.installed.or(wrapper.web).ok_or_else(|| {
        ProfileError::Auth(
            "Invalid credentials.json: missing 'installed' or 'web' object".to_string(),
        )
    })?;

    if is_placeholder_config(&config) {
        let message = missing_credentials_message();
        AUTH_MISSING_CREDENTIALS_LOG_ONCE.call_once(|| {
            eprintln!("[auth] {}", message.replace('\n', "\n[auth] "));
        });
        return Err(ProfileError::MissingCredentials(message));
    }

    Ok(config)
}

pub fn validate_google_credentials(settings: &AuthFlowSettings) -> Result<()> {
    load_google_oauth_config(settings).map(|_| ())
}