feat: added registration cli and route and cfg
defaults to disabled registrations ofc
This commit is contained in:
parent
401ef08af3
commit
bd9b9782b4
7 changed files with 190 additions and 50 deletions
|
@ -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<String>,
|
||||
|
||||
/// summary text for new user
|
||||
#[arg(long = "summary")]
|
||||
summary: Option<String>,
|
||||
|
||||
/// url for avatar image of new user
|
||||
#[arg(long = "avatar")]
|
||||
avatar_url: Option<String>,
|
||||
|
||||
/// url for banner image of new user
|
||||
#[arg(long = "banner")]
|
||||
banner_url: Option<String>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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?),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
summary: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
banner_url: Option<String>,
|
||||
) -> 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
|
||||
|
|
|
@ -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<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub frontend: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub frontend: Option<String>,
|
||||
pub allow_registration: bool,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
impl Config {
|
||||
pub fn load(path: Option<std::path::PathBuf>) -> Self {
|
||||
let Some(cfg_path) = path else { return Config::default() };
|
||||
|
|
|
@ -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<chrono::Utc>,
|
||||
}
|
||||
|
||||
pub async fn login(State(ctx): State<Context>, Json(login): Json<LoginForm>) -> crate::Result<Json<AuthSuccess>> {
|
||||
pub async fn login(
|
||||
State(ctx): State<Context>,
|
||||
Json(login): Json<LoginForm>
|
||||
) -> crate::Result<Json<AuthSuccess>> {
|
||||
// TODO salt the pwd
|
||||
match model::credential::Entity::find()
|
||||
.filter(Condition::all()
|
||||
|
@ -53,3 +56,33 @@ pub async fn login(State(ctx): State<Context>, Json(login): Json<LoginForm>) ->
|
|||
None => Err(UpubError::unauthorized()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct RegisterForm {
|
||||
username: String,
|
||||
password: String,
|
||||
display_name: Option<String>,
|
||||
summary: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
banner_url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
State(ctx): State<Context>,
|
||||
Json(registration): Json<RegisterForm>
|
||||
) -> crate::Result<Json<String>> {
|
||||
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)))
|
||||
}
|
||||
|
|
|
@ -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<crate::server::Context> {
|
|||
.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))
|
||||
|
|
82
src/server/admin.rs
Normal file
82
src/server/admin.rs
Normal file
|
@ -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<String>,
|
||||
summary: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
banner_url: Option<String>,
|
||||
) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
#[axum::async_trait]
|
||||
impl Administrable for super::Context {
|
||||
async fn register_user(
|
||||
&self,
|
||||
username: String,
|
||||
password: String,
|
||||
display_name: Option<String>,
|
||||
summary: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
banner_url: Option<String>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod admin;
|
||||
pub mod context;
|
||||
pub mod dispatcher;
|
||||
pub mod fetcher;
|
||||
|
|
Loading…
Reference in a new issue