reinhardt-auth 0.1.2

Authentication and authorization system
Documentation
//! Superuser creation support for management commands.
//!
//! Provides type-erased superuser creation that works with any user model.
//!
//! # Architecture
//!
//! The [`SuperuserInit`] trait is automatically implemented by the `#[user]`
//! macro, providing compile-time field name resolution. This ensures correct
//! behavior even when fields are renamed via `#[user_field(...)]` (e.g.,
//! `date_joined` mapped to `created_at`).
//!
//! The [`SuperuserCreator`] trait provides the async database persistence
//! layer, and [`TypedSuperuserCreator<U>`] bridges the two by combining
//! [`SuperuserInit`] for construction with `Model` for ORM operations.
//!
//! # Registration
//!
//! When `#[user]` and `#[model]` are both applied to a struct, the framework
//! automatically registers a [`SuperuserCreator`] via the `inventory` crate.
//! No manual registration is needed. This works for both full user models
//! (with `full = true`) and minimal user models — fields that are absent
//! from the user struct (e.g., `email`, `is_staff`, `date_joined`) are
//! simply skipped when constructing the superuser.
//!
//! For advanced use cases (e.g., custom creator logic), manual registration
//! is still supported and takes priority over auto-registration:
//!
//! ## Recommended: Using `#[user]` macro
//!
//! The simplest path is to use `#[user(...)]` with `#[model]`, which
//! auto-generates [`SuperuserInit`]:
//!
//! ```rust,ignore
//! use reinhardt_auth::{register_superuser_creator, superuser_creator_for};
//! use myapp::models::CustomUser;
//!
//! // CustomUser has #[user(hasher = Argon2Hasher, username_field = "username")]
//! // and #[model(table_name = "auth_user")] — SuperuserInit is auto-generated.
//! register_superuser_creator(superuser_creator_for::<CustomUser>());
//! ```
//!
//! ## Manual `BaseUser` implementors
//!
//! If you implement [`BaseUser`] manually without the `#[user]` macro,
//! you can still use `createsuperuser` by implementing [`SuperuserInit`]
//! yourself. See the [`SuperuserInit`] documentation for an example.

use async_trait::async_trait;
use std::marker::PhantomData;
use std::sync::OnceLock;

use crate::core::BaseUser;

/// Trait for initializing a user struct as a superuser.
///
/// # Auto-generation (recommended)
///
/// This trait is automatically implemented by the `#[user]` attribute macro
/// when `#[model]` is also present, regardless of whether `full = true` is
/// set. Setters for optional fields (`email`, `is_staff`, `date_joined`,
/// etc.) are emitted as no-ops when the field is absent, so minimal user
/// structs are supported. The macro uses compile-time field name resolution,
/// so field renames via `#[user_field(...)]` are handled correctly.
///
/// # Manual implementation
///
/// Projects that implement [`BaseUser`] manually (without the `#[user]`
/// macro) can implement this trait directly. The struct must also
/// implement `Default`.
///
/// ```rust,ignore
/// use reinhardt_auth::SuperuserInit;
///
/// impl SuperuserInit for MyUser {
///     fn init_superuser(username: &str, email: &str) -> Self {
///         Self {
///             username: username.to_string(),
///             email: email.to_string(),
///             is_superuser: true,
///             is_staff: true,
///             is_active: true,
///             date_joined: chrono::Utc::now(),
///             ..Default::default()
///         }
///     }
/// }
/// ```
///
/// Then register in `manage.rs`:
///
/// ```rust,ignore
/// register_superuser_creator(superuser_creator_for::<MyUser>());
/// ```
pub trait SuperuserInit: Default + Send + Sync {
	/// Create a new instance configured as a superuser.
	///
	/// Sets the username, email, and superuser/staff/active flags.
	/// The timestamp field (date_joined or equivalent) is set to the
	/// current time. Password is not set — call
	/// [`BaseUser::set_password`] separately.
	fn init_superuser(username: &str, email: &str) -> Self;
}

/// Type-erased superuser creation interface.
///
/// Implementations handle both user construction and database persistence.
#[async_trait]
pub trait SuperuserCreator: Send + Sync {
	/// Create a superuser in the database.
	///
	/// # Arguments
	///
	/// * `username` - The username for the new superuser
	/// * `email` - The email address for the new superuser
	/// * `password` - Optional password (hashed using the user model's configured hasher)
	async fn create_superuser(
		&self,
		username: &str,
		email: &str,
		password: Option<&str>,
	) -> Result<(), Box<dyn std::error::Error>>;
}

/// Generic [`SuperuserCreator`] for any user type with `#[user]` + `#[model]`.
///
/// `U` must implement:
/// - [`SuperuserInit`] (generated by `#[user]` macro)
/// - [`BaseUser`] (generated by `#[user]` macro — provides `set_password`)
/// - [`Model`](reinhardt_db::prelude::Model) (generated by `#[model]` macro — provides `objects().create()`)
pub struct TypedSuperuserCreator<U> {
	_phantom: PhantomData<U>,
}

impl<U> TypedSuperuserCreator<U> {
	/// Create a new typed superuser creator.
	pub fn new() -> Self {
		Self {
			_phantom: PhantomData,
		}
	}
}

impl<U> Default for TypedSuperuserCreator<U> {
	fn default() -> Self {
		Self::new()
	}
}

#[async_trait]
impl<U> SuperuserCreator for TypedSuperuserCreator<U>
where
	U: SuperuserInit
		+ BaseUser
		+ reinhardt_db::prelude::Model
		+ serde::Serialize
		+ for<'de> serde::Deserialize<'de>
		+ Clone
		+ Send
		+ Sync
		+ 'static,
{
	async fn create_superuser(
		&self,
		username: &str,
		email: &str,
		password: Option<&str>,
	) -> Result<(), Box<dyn std::error::Error>> {
		let mut user = U::init_superuser(username, email);

		if let Some(pwd) = password {
			user.set_password(pwd)?;
		}

		U::objects().create(&user).await?;
		Ok(())
	}
}

/// Create a [`SuperuserCreator`] for a specific user type.
///
/// The user type must have `#[user(...)]` and `#[model(...)]` macros applied.
///
/// # Example
///
/// ```rust,ignore
/// use reinhardt_auth::superuser_creator_for;
/// use myapp::models::CustomUser;
///
/// let creator = superuser_creator_for::<CustomUser>();
/// ```
pub fn superuser_creator_for<U>() -> Box<dyn SuperuserCreator>
where
	U: SuperuserInit
		+ BaseUser
		+ reinhardt_db::prelude::Model
		+ serde::Serialize
		+ for<'de> serde::Deserialize<'de>
		+ Clone
		+ Send
		+ Sync
		+ 'static,
{
	Box::new(TypedSuperuserCreator::<U>::new())
}

/// Global registry for the superuser creator.
static SUPERUSER_CREATOR: OnceLock<Box<dyn SuperuserCreator>> = OnceLock::new();

/// Register a [`SuperuserCreator`] for use by the `createsuperuser` command.
///
/// This should be called early in program startup (e.g., in `main()`)
/// before `execute_from_command_line`.
///
/// # Panics
///
/// Panics if called more than once.
pub fn register_superuser_creator(creator: Box<dyn SuperuserCreator>) {
	if SUPERUSER_CREATOR.set(creator).is_err() {
		panic!("register_superuser_creator called more than once");
	}
}

/// Retrieve the registered [`SuperuserCreator`].
///
/// Returns `None` if [`register_superuser_creator`] has not been called
/// and no auto-registration was found.
pub fn get_superuser_creator() -> Option<&'static dyn SuperuserCreator> {
	SUPERUSER_CREATOR.get().map(|b| b.as_ref())
}

// ============================================================================
// Auto-registration via inventory
// ============================================================================

/// Compile-time registration entry for auto-discovered superuser creators.
///
/// Submitted via `inventory::submit!` by the `#[user]` macro whenever
/// `#[model]` is also present. The framework collects all submissions at
/// startup and populates the global [`OnceLock`]. `full = true` is not
/// required.
///
/// You typically do not create this struct directly — the `#[user]` macro
/// generates the registration code automatically.
pub struct SuperuserCreatorRegistration {
	/// Factory function that produces a boxed [`SuperuserCreator`].
	pub create: fn() -> Box<dyn SuperuserCreator>,

	/// Type name of the user model (for diagnostics in duplicate detection).
	pub type_name: &'static str,
}

impl SuperuserCreatorRegistration {
	/// Internal constructor used by the `#[user]` macro.
	#[doc(hidden)]
	pub const fn __macro_new(
		create: fn() -> Box<dyn SuperuserCreator>,
		type_name: &'static str,
	) -> Self {
		Self { create, type_name }
	}
}

inventory::collect!(SuperuserCreatorRegistration);

/// Auto-register a [`SuperuserCreator`] from inventory submissions.
///
/// Called by the framework before command dispatch. If a creator was already
/// registered manually via [`register_superuser_creator`], this is a no-op
/// (preserving backwards compatibility) regardless of how many `#[user]` +
/// `#[model]` types exist in the inventory.
///
/// # Panics
///
/// Panics if multiple `#[user]` + `#[model]` types are found in the
/// inventory and no manual registration has been made. Only one user model
/// can serve as the auto-registered superuser creator; if you legitimately
/// have multiple user models, opt out by calling
/// [`register_superuser_creator`] manually before command dispatch.
pub fn auto_register_superuser_creator() {
	// Honor an existing manual registration first, regardless of how many
	// inventory entries exist. This makes manual registration a reliable
	// escape hatch for projects that intentionally define multiple
	// `#[user]` + `#[model]` types.
	if SUPERUSER_CREATOR.get().is_some() {
		return;
	}

	let registrations: Vec<_> = inventory::iter::<SuperuserCreatorRegistration>().collect();

	match registrations.len() {
		0 => {
			// No auto-registration available; manual registration or
			// a later error in get_superuser_creator() will handle this.
		}
		1 => {
			let reg = &registrations[0];
			let creator = (reg.create)();
			// Ignore set failure: another thread may have registered
			// between our check and set. The first one wins.
			let _ = SUPERUSER_CREATOR.set(creator);
		}
		_ => {
			let names: Vec<&str> = registrations.iter().map(|r| r.type_name).collect();
			panic!(
				"Multiple SuperuserCreator registrations found: {:?}. \
				 Only one user model may have #[user] together with #[model]. \
				 Remove the #[user] or #[model] attribute from all but one user model, \
				 or call register_superuser_creator() manually before command dispatch.",
				names
			);
		}
	}
}