pub struct AdminUser {
pub id: String,
pub username: String,
pub password_hash: String,
pub status: 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>,
}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,activefind_by_id/find_by_username: lookup (the latter is the login path)list_all: every operator, oldest firstset_password_hash/set_status/mark_logged_in: in-place updatesset_totp_pending/confirm_totp/clear_totp/claim_totp_step: the second factor’s lifecycle, and RFC 6238 §5.2’s replay guarddelete: remove, cascading to the operator’s sessions and recovery codesto_json: admin-facing rendering (never the password hash, never a secret)
Fields§
§id: String§username: StringAlways lowercase: AdminUser::create normalizes before writing, so
Alice and alice cannot become two logins that read as one.
password_hash: StringThe encoded KDF output – see crate::admin::password. Never rendered.
status: String§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>Implementations§
Source§impl AdminUser
impl AdminUser
Sourcepub async fn create(
username: &str,
password_hash: &str,
database: &Database,
) -> Result<AdminUser, Error>
pub async fn create( username: &str, password_hash: &str, 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 crate::admin::password: this
layer never sees a plaintext password and cannot hash one.
A duplicate username surfaces as the UNIQUE violation it is; the caller
(admin::users::create_user) checks first and reports it in words.
pub async fn find_by_id( id: &str, database: &Database, ) -> Result<Option<AdminUser>, Error>
Sourcepub async fn find_by_username(
username: &str,
database: &Database,
) -> Result<Option<AdminUser>, Error>
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.
Sourcepub async fn list_all(database: &Database) -> Result<Vec<AdminUser>, Error>
pub async fn list_all(database: &Database) -> Result<Vec<AdminUser>, Error>
Every operator, oldest first – admin user list.
Sourcepub async fn set_password_hash(
&mut self,
password_hash: &str,
database: &Database,
) -> Result<(), Error>
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.
Sourcepub async fn set_status(
&mut self,
status: &str,
database: &Database,
) -> Result<(), Error>
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.
Sourcepub async fn set_totp_pending(
&mut self,
secret: &[u8],
database: &Database,
) -> Result<(), Error>
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.
Sourcepub async fn confirm_totp(&mut self, database: &Database) -> Result<(), Error>
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.
Sourcepub async fn clear_totp(&mut self, database: &Database) -> Result<(), Error>
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.
Sourcepub async fn claim_totp_step(
&mut self,
step: i64,
database: &Database,
) -> Result<bool, Error>
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.
Sourcepub async fn mark_logged_in(&mut self, database: &Database) -> Result<(), Error>
pub async fn mark_logged_in(&mut self, database: &Database) -> Result<(), Error>
Stamps last_login_at. Advisory only – nothing authorises on it.
Called when a login completes, which for an operator with a second factor is one request later than the password being accepted.
Sourcepub async fn delete(id: &str, database: &Database) -> Result<bool, Error>
pub async fn delete(id: &str, 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::connect
and connect_in_memory both pin it. Returns whether a row existed.
Sourcepub fn has_totp(&self) -> bool
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.
Sourcepub fn has_pending_totp(&self) -> bool
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”.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for AdminUser
impl RefUnwindSafe for AdminUser
impl Send for AdminUser
impl Sync for AdminUser
impl Unpin for AdminUser
impl UnsafeUnpin for AdminUser
impl UnwindSafe for AdminUser
Blanket Implementations§
Source§impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
Source§impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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