diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 707ba1f..293646e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -10,8 +10,8 @@ pub use faker::*; mod relay; pub use relay::*; -//mod register; -//pub use register::*; +mod register; +pub use register::*; mod update; pub use update::*; @@ -64,6 +64,32 @@ pub enum CliCommand { #[arg(long, short, default_value_t = 7)] /// number of days after which users should get updated days: i64, + }, + + /// register a new local user + Register { + /// username for new user, must be unique locally and cannot be changed + username: String, + + /// password for new user + // TODO get this with getpass rather than argv!!!! + password: String, + + /// display name for new user + #[arg(long = "name")] + display_name: Option, + + /// summary text for new user + #[arg(long = "summary")] + summary: Option, + + /// url for avatar image of new user + #[arg(long = "avatar")] + avatar_url: Option, + + /// url for banner image of new user + #[arg(long = "banner")] + banner_url: Option, } } @@ -87,5 +113,7 @@ pub async fn run( Ok(fix(ctx, likes, shares, replies).await?), CliCommand::Update { days } => Ok(update_users(ctx, days).await?), + CliCommand::Register { username, password, display_name, summary, avatar_url, banner_url } => + Ok(register(ctx, username, password, display_name, summary, avatar_url, banner_url).await?), } } diff --git a/src/cli/register.rs b/src/cli/register.rs index 2f84d80..744e9d0 100644 --- a/src/cli/register.rs +++ b/src/cli/register.rs @@ -2,37 +2,24 @@ use openssl::rsa::Rsa; use sea_orm::{EntityTrait, IntoActiveModel}; pub async fn register( - db: sea_orm::DatabaseConnection, - domain: String, + ctx: crate::server::Context, + username: String, + password: String, + display_name: Option, + summary: Option, + avatar_url: Option, + banner_url: Option, ) -> crate::Result<()> { - let key = Rsa::generate(2048).unwrap(); - let test_user = crate::model::user::Model { - id: format!("{domain}/users/test"), - name: Some("μpub".into()), - domain: clean_domain(&domain), - preferred_username: "test".to_string(), - summary: Some("hello world! i'm manually generated but served dynamically from db! check progress at https://git.alemi.dev/upub.git".to_string()), - following: None, - following_count: 0, - followers: None, - followers_count: 0, - statuses_count: 0, - icon: Some("https://cdn.alemi.dev/social/circle-square.png".to_string()), - image: Some("https://cdn.alemi.dev/social/someriver-xs.jpg".to_string()), - inbox: None, - shared_inbox: None, - outbox: None, - actor_type: apb::ActorType::Person, - created: chrono::Utc::now(), - updated: chrono::Utc::now(), - private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()), - // TODO generate a fresh one every time - public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(), - }; + ctx.register_user( + username.clone(), + password, + display_name, + summary, + avatar_url, + banner_url, + ).await?; - crate::model::user::Entity::insert(test_user.clone().into_active_model()).exec(&db).await?; - - Ok(()) + tracing::info!("registered new user: {username}"); } // TODO duplicated, make an util?? idk diff --git a/src/config.rs b/src/config.rs index 8be6af3..076015d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,9 +9,31 @@ pub struct Config { #[serde(default)] pub datasource: DatasourceConfig, + #[serde(default)] + pub security: SecurityConfig, + // TODO should i move app keys here? } +#[serde_inline_default::serde_inline_default] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)] +pub struct InstanceConfig { + #[serde_inline_default("μpub".into())] + pub name: String, + + #[serde_inline_default("micro social network, federated".into())] + pub description: String, + + #[serde_inline_default("upub.social".into())] + pub domain: String, + + #[serde(default)] + pub contact: Option, + + #[serde(default)] + pub frontend: Option, +} + #[serde_inline_default::serde_inline_default] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)] pub struct DatasourceConfig { @@ -39,26 +61,12 @@ pub struct DatasourceConfig { #[serde_inline_default::serde_inline_default] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)] -pub struct InstanceConfig { - #[serde_inline_default("μpub".into())] - pub name: String, - - #[serde_inline_default("micro social network, federated".into())] - pub description: String, - - #[serde_inline_default("upub.social".into())] - pub domain: String, - +pub struct SecurityConfig { #[serde(default)] - pub contact: Option, - - #[serde(default)] - pub frontend: Option, + pub allow_registration: bool, } - - impl Config { pub fn load(path: Option) -> Self { let Some(cfg_path) = path else { return Config::default() }; diff --git a/src/routes/activitypub/auth.rs b/src/routes/activitypub/auth.rs index 9a07abd..2f5239e 100644 --- a/src/routes/activitypub/auth.rs +++ b/src/routes/activitypub/auth.rs @@ -2,7 +2,7 @@ use axum::{http::StatusCode, extract::State, Json}; use rand::Rng; use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter}; -use crate::{errors::UpubError, model, server::Context}; +use crate::{errors::UpubError, model, server::{admin::Administrable, Context}}; #[derive(Debug, Clone, serde::Deserialize)] @@ -18,7 +18,10 @@ pub struct AuthSuccess { expires: chrono::DateTime, } -pub async fn login(State(ctx): State, Json(login): Json) -> crate::Result> { +pub async fn login( + State(ctx): State, + Json(login): Json +) -> crate::Result> { // TODO salt the pwd match model::credential::Entity::find() .filter(Condition::all() @@ -53,3 +56,33 @@ pub async fn login(State(ctx): State, Json(login): Json) -> None => Err(UpubError::unauthorized()), } } + +#[derive(Debug, Clone, serde::Deserialize)] +pub struct RegisterForm { + username: String, + password: String, + display_name: Option, + summary: Option, + avatar_url: Option, + banner_url: Option, +} + +pub async fn register( + State(ctx): State, + Json(registration): Json +) -> crate::Result> { + if !ctx.cfg().security.allow_registration { + return Err(UpubError::forbidden()); + } + + ctx.register_user( + registration.username.clone(), + registration.password, + registration.display_name, + registration.summary, + registration.avatar_url, + registration.banner_url + ).await?; + + Ok(Json(ctx.uid(registration.username))) +} diff --git a/src/routes/activitypub/mod.rs b/src/routes/activitypub/mod.rs index 43d3bb0..8b35705 100644 --- a/src/routes/activitypub/mod.rs +++ b/src/routes/activitypub/mod.rs @@ -11,7 +11,7 @@ pub mod well_known; pub mod jsonld; pub use jsonld::JsonLD; -use axum::{http::StatusCode, response::IntoResponse, routing::{get, post}, Router}; +use axum::{http::StatusCode, response::IntoResponse, routing::{get, post, put}, Router}; pub trait ActivityPubRouter { fn ap_routes(self) -> Self; @@ -35,6 +35,7 @@ impl ActivityPubRouter for Router { .route("/outbox/page", get(ap::outbox::page)) // AUTH routes .route("/auth", post(ap::auth::login)) + .route("/auth", put(ap::auth::register)) // .well-known and discovery .route("/.well-known/webfinger", get(ap::well_known::webfinger)) .route("/.well-known/host-meta", get(ap::well_known::host_meta)) diff --git a/src/server/admin.rs b/src/server/admin.rs new file mode 100644 index 0000000..d20fdd3 --- /dev/null +++ b/src/server/admin.rs @@ -0,0 +1,82 @@ +use sea_orm::{EntityTrait, IntoActiveModel}; + +#[axum::async_trait] +pub trait Administrable { + async fn register_user( + &self, + username: String, + password: String, + display_name: Option, + summary: Option, + avatar_url: Option, + banner_url: Option, + ) -> crate::Result<()>; +} + +#[axum::async_trait] +impl Administrable for super::Context { + async fn register_user( + &self, + username: String, + password: String, + display_name: Option, + summary: Option, + avatar_url: Option, + banner_url: Option, + ) -> crate::Result<()> { + let key = openssl::rsa::Rsa::generate(2048).unwrap(); + let ap_id = self.uid(username.clone()); + let db = self.db(); + let domain = self.domain().to_string(); + let user_model = crate::model::user::Model { + id: ap_id.clone(), + name: display_name, + domain, summary, + preferred_username: username.clone(), + following: None, + following_count: 0, + followers: None, + followers_count: 0, + statuses_count: 0, + icon: avatar_url, + image: banner_url, + inbox: None, + shared_inbox: None, + outbox: None, + actor_type: apb::ActorType::Person, + created: chrono::Utc::now(), + updated: chrono::Utc::now(), + private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()), + public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(), + }; + + crate::model::user::Entity::insert(user_model.into_active_model()) + .exec(db) + .await?; + + let config_model = crate::model::config::Model { + id: ap_id.clone(), + accept_follow_requests: true, + show_followers_count: true, + show_following_count: true, + show_followers: false, + show_following: false, + }; + + crate::model::config::Entity::insert(config_model.into_active_model()) + .exec(db) + .await?; + + let credentials_model = crate::model::credential::Model { + id: ap_id, + email: username, + password, + }; + + crate::model::credential::Entity::insert(credentials_model.into_active_model()) + .exec(db) + .await?; + + Ok(()) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index c5383df..b6830b3 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,3 +1,4 @@ +pub mod admin; pub mod context; pub mod dispatcher; pub mod fetcher;