Skip to main content

AdminUser

Struct AdminUser 

Source
pub struct AdminUser {
Show 13 fields pub id: Uuid, pub username: String, pub password_hash: String, pub status: String, pub role: Option<String>, pub totp_secret: Option<Vec<u8>>, pub totp_pending_secret: Option<Vec<u8>>, pub totp_last_step: Option<i64>, pub created_at: i64, pub updated_at: i64, pub last_login_at: Option<i64>, pub contact_email: Option<String>, pub known_login_ips: Vec<String>,
}
Expand description

An operator of the web admin interface.

Not an ACME concept and never joined to one: an AdminUser is a person with a password, an accounts row is a client key. There is no profile column – an admin user sees every endpoint this process serves.

§Methods

  • create: persist a new operator, active
  • find_by_id / find_by_username: lookup (the latter is the login path)
  • list_all: every operator, oldest first
  • set_password_hash / set_status / set_role / mark_logged_in: in-place updates
  • role: the privilege tier, NULL resolved to AdminRole::Admin
  • set_totp_pending / confirm_totp / clear_totp / claim_totp_step: the second factor’s lifecycle, and RFC 6238 §5.2’s replay guard
  • delete: remove, cascading to the operator’s sessions and recovery codes
  • to_json: admin-facing rendering (never the password hash, never a secret)

Fields§

§id: Uuid§username: String

Always lowercase: AdminUser::create normalizes before writing, so Alice and alice cannot become two logins that read as one.

§password_hash: String

The encoded KDF output – see admin::password. Never rendered.

§status: String§role: Option<String>

The privilege tier, raw from the column. None is a row that predates the role column and reads as AdminRole::Admin; use AdminUser::role rather than matching this directly.

§totp_secret: Option<Vec<u8>>

Set once the owner has proven a code against a pending enrolment. None means no second factor is configured.

§totp_pending_secret: Option<Vec<u8>>

An enrolment begun but not yet confirmed. Not a usable second factor.

§totp_last_step: Option<i64>

The last TOTP time step accepted, so a code cannot be replayed inside its own window.

§created_at: i64§updated_at: i64§last_login_at: Option<i64>§contact_email: Option<String>

Where to send this operator security notifications (a completed sign-in from an unfamiliar address, a refused second factor, a credential change). None means none are sent – the event is still logged.

§known_login_ips: Vec<String>

The operator’s recent distinct login addresses, most-recent-first, capped at KNOWN_LOGIN_IPS. Compared against the live request, but only to decide whether to notify – never to authorise. Persisted as a JSON array (the accounts.contact convention).

Implementations§

Source§

impl AdminUser

Source

pub async fn create( username: &str, password_hash: &str, role: Option<AdminRole>, database: &Database, ) -> Result<AdminUser, Error>

Persists a new operator, active. username is lowercased and trimmed here rather than at the call sites, so every path – the CLI, a future API – stores the same thing.

password_hash is already encoded by admin::password: this layer never sees a plaintext password and cannot hash one.

role is written in the same INSERT. None leaves the column NULL, which reads as AdminRole::Admin – the safe default for the bootstrap operator, and what every row created before the column existed holds. It used to be the only option, with admin::users::create_user calling AdminUser::set_role afterwards for a narrower tier; that made admin user create --role viewer two writes, so a failure between them left an operator at full admin with their password already set.

A duplicate username surfaces as the UNIQUE violation it is; the caller (admin::users::create_user) checks first and reports it in words.

Source

pub async fn find_by_id( id: Uuid, database: &Database, ) -> Result<Option<AdminUser>, Error>

Looks an operator up by id: the session path, which carries the id.

Source

pub async fn find_by_username( username: &str, database: &Database, ) -> Result<Option<AdminUser>, Error>

The login path. Lowercases the argument for the same reason AdminUser::create does – a login typed Alice must find alice.

Source

pub async fn list_all(database: &Database) -> Result<Vec<AdminUser>, Error>

Every operator, oldest first.

A scan, not a listing: its one caller is admin::mfa::operators_without_a_factor, which counts the operators with no confirmed factor for the admin.require_mfa startup warning. Nothing renders it, which is why it can sit beside AdminUser::search without being the second listing the paging pass deleted Account::list_all for – an order nothing displays cannot disagree with the paged one.

Source

pub async fn search( limit: i64, offset: i64, database: &Database, ) -> Result<(Vec<AdminUser>, i64), Error>

One page of admin user list, plus the total the table holds unpaged.

Oldest first, and the one listing in the binary that is: every other paged listing puts the newest row on top, but the bootstrap operator – the one created before the panel could be signed in to at all – is precisely the row whose position should not move as colleagues are added. id breaks the created_at tie for Eab::search’s reason: created_at is a whole second, and operators are created in one go.

Source

pub async fn set_password_hash( &mut self, password_hash: &str, database: &Database, ) -> Result<(), Error>

Replaces the stored hash. Callers are responsible for invalidating the owner’s sessions – admin::users::set_password does, and a password change that left them alive would be a change in name only.

Source

pub async fn set_status( &mut self, status: &str, database: &Database, ) -> Result<(), Error>

Moves between active and disabled. A disabled operator cannot log in, and an existing session of theirs is refused on its next use – the session rows are left for the reaper rather than deleted here, so re-enabling is a single UPDATE either way.

Source

pub async fn set_role( &mut self, role: AdminRole, database: &Database, ) -> Result<(), Error>

Sets the privilege tier. Callers revoke the operator’s sessions – admin::users::set_role does, matching a disable and a password change; the write extractors also re-read role every request, so a demotion takes effect on the next call regardless.

Source

pub async fn set_contact_email( &mut self, email: Option<&str>, database: &Database, ) -> Result<(), Error>

Sets (or clears, with None) the address this operator receives security notifications at. Not a credential – no session is revoked. The address shape is the caller’s to validate (admin::users::set_contact_email); this layer only stores what it is handed.

Source

pub async fn set_totp_pending( &mut self, secret: &[u8], database: &Database, ) -> Result<(), Error>

Stores an enrolment the owner has not yet proven a code against.

Not a usable second factor: AdminUser::has_totp stays false until AdminUser::confirm_totp moves it across, which is what stops an abandoned enrolment from locking its own owner out.

Source

pub async fn confirm_totp(&mut self, database: &Database) -> Result<(), Error>

Promotes the pending secret to the real one.

One statement, deliberately: a half-applied enrolment would leave the operator believing they have a factor that nothing checks, or holding a pending secret alongside a live one. totp_last_step is cleared with them – the replay guard belongs to the secret it was recorded against.

A no-op when nothing is pending, so a double-submit cannot clear a live factor.

Source

pub async fn clear_totp(&mut self, database: &Database) -> Result<(), Error>

Removes the factor, any half-finished enrolment and the replay guard together. Callers drop the recovery codes too – a code that recovers access to a factor that no longer exists is a second password.

Source

pub async fn claim_totp_step( &mut self, step: i64, database: &Database, ) -> Result<bool, Error>

Records step as accepted, refusing one that is not strictly newer than the stored value – RFC 6238 §5.2’s replay guard.

The comparison lives in the WHERE clause rather than in Rust: a code observed in flight and resubmitted inside its own 30-second window must not be accepted twice, and with two requests racing it is rows_affected that decides which one was first. Same primitive as Nonce::verify.

Source

pub async fn mark_logged_in( &mut self, client_ip: Option<&str>, database: &Database, ) -> Result<(), Error>

Stamps last_login_at, and folds client_ip into known_login_ips (move-to-front, deduplicated, capped at KNOWN_LOGIN_IPS). Advisory only – nothing authorises on either column; the address set exists so a sign-in from an unfamiliar address can be noticed.

Called when a login completes, which for an operator with a second factor is one request later than the password being accepted. A caller that needs the pre-login address set (to decide whether this sign-in is from a new address) must read known_login_ips before calling.

Source

pub async fn delete(id: Uuid, database: &Database) -> Result<bool, Error>

Removes the operator. Their sessions go with them via the schema’s ON DELETE CASCADE, which needs foreign_keys on – Database::open and connect_in_memory both pin it. Returns whether a row existed.

Source

pub fn is_active(&self) -> bool

Whether this operator may log in and hold a session.

Source

pub fn role(&self) -> AdminRole

The privilege tier, with NULL resolved to AdminRole::Admin. Match on this, never on the raw AdminUser::role field.

Source

pub fn has_totp(&self) -> bool

Whether a confirmed second factor is configured. A pending enrolment does not count – it has never been proven against a code.

Source

pub fn has_pending_totp(&self) -> bool

Whether an enrolment is half-finished: a secret was generated and shown, and no code has proven it yet.

Deliberately not folded into AdminUser::has_totp and deliberately not in AdminUser::to_json: the login path must treat this operator as having no factor, and the only surface that cares is the enrolment page deciding whether to offer “start over”.

Source

pub fn to_json(&self) -> Value

The admin-facing rendering. Never includes password_hash, nor either TOTP secret – only whether one is configured.

Trait Implementations§

Source§

impl Clone for AdminUser

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for AdminUser

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more