mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat(!783): Initial implementation
Adds support for extra limited-use registration tokens stored in the database, and a new service to manage them.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
|
||||
use conduwuit::utils::stream::{ReadyExt, TryIgnore};
|
||||
use database::{Database, Deserialized, Json, Map};
|
||||
use futures::Stream;
|
||||
use ruma::OwnedUserId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(super) struct Data {
|
||||
registrationtoken_info: Arc<Map>,
|
||||
}
|
||||
|
||||
/// Metadata of a registration token.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DatabaseTokenInfo {
|
||||
/// The admin user who created this token.
|
||||
pub creator: OwnedUserId,
|
||||
/// The number of times this token has been used to create an account.
|
||||
pub uses: u64,
|
||||
/// When this token will expire, if it expires.
|
||||
pub expires: Option<TokenExpires>,
|
||||
}
|
||||
|
||||
impl DatabaseTokenInfo {
|
||||
pub(super) fn new(creator: OwnedUserId, expires: Option<TokenExpires>) -> Self {
|
||||
Self { creator, uses: 0, expires }
|
||||
}
|
||||
|
||||
/// Determine whether this token info represents a valid token, i.e. one
|
||||
/// that has not expired according to its [`Self::expires`] property. If
|
||||
/// [`Self::expires`] is [`None`], this function will always return `true`.
|
||||
#[must_use]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
match self.expires {
|
||||
| Some(TokenExpires::AfterUses(max_uses)) => self.uses <= max_uses,
|
||||
| Some(TokenExpires::AfterTime(expiry_time)) => {
|
||||
let now = SystemTime::now();
|
||||
|
||||
expiry_time >= now
|
||||
},
|
||||
| None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum TokenExpires {
|
||||
AfterUses(u64),
|
||||
AfterTime(SystemTime),
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub(super) fn new(db: &Arc<Database>) -> Self {
|
||||
Self {
|
||||
registrationtoken_info: db["registrationtoken_info"].clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate a registration token with its metadata in the database.
|
||||
pub(super) fn save_token(&self, token: &str, info: &DatabaseTokenInfo) {
|
||||
self.registrationtoken_info.put(token, Json(info));
|
||||
}
|
||||
|
||||
/// Delete a registration token.
|
||||
pub(super) fn revoke_token(&self, token: &str) { self.registrationtoken_info.del(token); }
|
||||
|
||||
/// Look up a registration token's metadata.
|
||||
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<DatabaseTokenInfo> {
|
||||
self.registrationtoken_info
|
||||
.qry(token)
|
||||
.await
|
||||
.deserialized()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Iterate over all valid tokens and delete expired ones.
|
||||
pub(super) fn iterate_and_clean_tokens(
|
||||
&self,
|
||||
) -> impl Stream<Item = (&str, DatabaseTokenInfo)> + Send + '_ {
|
||||
self.registrationtoken_info
|
||||
.stream()
|
||||
.ignore_err()
|
||||
.ready_filter(|item: &(&str, DatabaseTokenInfo)| {
|
||||
if item.1.is_valid() {
|
||||
true
|
||||
} else {
|
||||
self.registrationtoken_info.del(item.0);
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
mod data;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use conduwuit::{Err, Result, utils};
|
||||
use data::Data;
|
||||
pub use data::{DatabaseTokenInfo, TokenExpires};
|
||||
use futures::{Stream, StreamExt, stream};
|
||||
use ruma::OwnedUserId;
|
||||
|
||||
use crate::{Dep, config};
|
||||
|
||||
const RANDOM_TOKEN_LENGTH: usize = 64;
|
||||
|
||||
pub struct Service {
|
||||
db: Data,
|
||||
services: Services,
|
||||
}
|
||||
|
||||
struct Services {
|
||||
config: Dep<config::Service>,
|
||||
}
|
||||
|
||||
/// A validated registration token which may be used to create an account.
|
||||
pub struct ValidToken<'token> {
|
||||
pub token: &'token str,
|
||||
pub source: ValidTokenSource,
|
||||
}
|
||||
|
||||
impl PartialEq<str> for ValidToken<'_> {
|
||||
fn eq(&self, other: &str) -> bool { self.token == other }
|
||||
}
|
||||
|
||||
/// The source of a valid database token.
|
||||
pub enum ValidTokenSource {
|
||||
/// The static token set in the homeserver's config file, which is
|
||||
/// always valid.
|
||||
ConfigFile,
|
||||
/// A database token which has been checked to be valid.
|
||||
Database(DatabaseTokenInfo),
|
||||
}
|
||||
|
||||
impl crate::Service for Service {
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
db: Data::new(args.db),
|
||||
services: Services {
|
||||
config: args.depend::<config::Service>("config"),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Issue a new registration token and save it in the database.
|
||||
pub fn issue_token(
|
||||
&self,
|
||||
creator: OwnedUserId,
|
||||
expires: Option<TokenExpires>,
|
||||
) -> (String, DatabaseTokenInfo) {
|
||||
let token = utils::random_string(RANDOM_TOKEN_LENGTH);
|
||||
let info = DatabaseTokenInfo::new(creator, expires);
|
||||
|
||||
self.db.save_token(&token, &info);
|
||||
(token, info)
|
||||
}
|
||||
|
||||
/// Get the registration token set in the config file, if it exists.
|
||||
pub fn get_config_file_token(&self) -> Option<ValidToken<'_>> {
|
||||
self.services
|
||||
.config
|
||||
.registration_token
|
||||
.as_deref()
|
||||
.map(|token| ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::ConfigFile,
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate a registration token.
|
||||
pub async fn validate_token<'token>(&self, token: &'token str) -> Option<ValidToken<'token>> {
|
||||
// Check the registration token in the config first
|
||||
if self
|
||||
.get_config_file_token()
|
||||
.is_some_and(|valid_token| valid_token == *token)
|
||||
{
|
||||
return Some(ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::ConfigFile,
|
||||
});
|
||||
}
|
||||
|
||||
// Now check the database
|
||||
if let Some(token_info) = self.db.lookup_token_info(token).await
|
||||
&& token_info.is_valid()
|
||||
{
|
||||
return Some(ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::Database(token_info),
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise it's not valid
|
||||
None
|
||||
}
|
||||
|
||||
/// Mark a valid token as having been used to create a new account.
|
||||
pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken<'_>) {
|
||||
match source {
|
||||
| ValidTokenSource::ConfigFile => {
|
||||
// we don't track uses of the config file token, do nothing
|
||||
},
|
||||
| ValidTokenSource::Database(mut info) => {
|
||||
info.uses = info.uses.saturating_add(1);
|
||||
|
||||
self.db.save_token(token, &info);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to revoke a valid token.
|
||||
///
|
||||
/// Note that some tokens (like the one set in the config file) cannot be
|
||||
/// revoked.
|
||||
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken<'_>) -> Result {
|
||||
match source {
|
||||
| ValidTokenSource::ConfigFile => {
|
||||
// the config file token cannot be revoked
|
||||
Err!(
|
||||
"The token set in the config file cannot be revoked. Edit the config file \
|
||||
to change it."
|
||||
)
|
||||
},
|
||||
| ValidTokenSource::Database(_) => {
|
||||
self.db.revoke_token(token);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all valid registration tokens.
|
||||
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken<'_>> + Send + '_ {
|
||||
stream::iter(self.get_config_file_token()).chain(self.db.iterate_and_clean_tokens().map(
|
||||
|(token, info)| ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::Database(info),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user