User creation now possible!
Signed-off-by: Louis Hollingworth <louis@hollingworth.nl>
This commit is contained in:
parent
215cf639bd
commit
f671a5ba2f
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
/target
|
||||
.envrc
|
||||
/data
|
||||
.blogrss.toml
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO users (id, name, epost, xmpp, site, password, active_token)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) \n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4b300d7e44f7b6c95cfb9a2791e81abb21ecf887cb7cf46e5dcb45567818cab4"
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT * FROM users\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "site",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "xmpp",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "epost",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "active_token",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a2c624a66b1999281d3b63e85b77785d0aae8d5ebc4865b7d42d7afd81e6f58c"
|
||||
}
|
1835
Cargo.lock
generated
1835
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -2,6 +2,11 @@
|
|||
name = "blogrss"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Louis Hollingworth <louis@hollingworth.nl>"]
|
||||
readme = "README.md"
|
||||
repository = "https://git.ludoviko.ch/lucxjo/blogrss"
|
||||
license = "AGPL-3.0-or-later"
|
||||
description = "A simple blogging system using RSS/Atom"
|
||||
|
||||
[lib]
|
||||
|
||||
|
@ -14,9 +19,18 @@ name = "blogrss-server"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
argon2 = "0.5.2"
|
||||
atom_syndication = { version = "0.12.2", features = ["with-serde"] }
|
||||
axum = "0.7.3"
|
||||
axum-xml = "0.2.0"
|
||||
chrono = "0.4.31"
|
||||
clap = { version = "4.4.16", features = ["derive"] }
|
||||
jsonwebtoken = "9.2.0"
|
||||
rand_core = { version = "0.6.4", features = ["getrandom"] }
|
||||
reqwest = { version = "0.11.23", features = ["json", "native-tls-vendored"] }
|
||||
rss = { version = "2.0.6", features = ["serde", "atom", "atom_syndication", "chrono", "with-serde"] }
|
||||
rustcrypto = "0.0.0"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
sqlx = { version = "0.7.3", features = ["runtime-tokio", "sqlite"] }
|
||||
tokio = { version = "1.35.1", features = ["full"] }
|
||||
toml = "0.8.8"
|
||||
|
|
2
migrations/0001_add_user.down.sql
Normal file
2
migrations/0001_add_user.down.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
-- Add down migration script here
|
||||
DROP TABLE "users";
|
9
migrations/0001_add_user.up.sql
Normal file
9
migrations/0001_add_user.up.sql
Normal file
|
@ -0,0 +1,9 @@
|
|||
-- Add up migration script here
|
||||
CREATE TABLE "users" (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
site TEXT NOT NULL,
|
||||
xmpp TEXT,
|
||||
epost TEXT,
|
||||
password TEXT NOT NULL
|
||||
);
|
2
migrations/0002_add_token_to_user.down.sql
Normal file
2
migrations/0002_add_token_to_user.down.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
-- Add down migration script here
|
||||
ALTER TABLE users DROP COLUMN active_token;
|
3
migrations/0002_add_token_to_user.up.sql
Normal file
3
migrations/0002_add_token_to_user.up.sql
Normal file
|
@ -0,0 +1,3 @@
|
|||
-- Add up migration script here
|
||||
ALTER TABLE users
|
||||
ADD COLUMN active_token TEXT;
|
|
@ -1,13 +1,90 @@
|
|||
use argon2::PasswordHasher;
|
||||
use blogrss::{User, RegisterUser};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = axum::Router::new()
|
||||
.route("/", axum::routing::get(test_feed));
|
||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
let pool = sqlx::SqlitePool::connect(&database_url).await.unwrap();
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
|
||||
let app = axum::Router::new()
|
||||
.route("/test", axum::routing::get(test_feed))
|
||||
.route("/user/register", axum::routing::post(register_user))
|
||||
.with_state(pool);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
struct JWTClaims {
|
||||
sub: String,
|
||||
exp: usize,
|
||||
}
|
||||
|
||||
async fn register_user(
|
||||
axum::extract::State(pool): axum::extract::State<sqlx::SqlitePool>,
|
||||
axum::Json(user): axum::Json<RegisterUser>,
|
||||
) -> Result<String, (axum::http::StatusCode, String)> {
|
||||
let recs = sqlx::query_as!(
|
||||
User,
|
||||
r#"
|
||||
SELECT * FROM users
|
||||
"#
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await;
|
||||
match recs {
|
||||
Err(_) => {
|
||||
return Err((
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Error getting users".to_string(),
|
||||
))
|
||||
}
|
||||
Ok(r) => {
|
||||
if r.len() == 0 {
|
||||
if user.epost.is_none() && user.xmpp.is_none() {
|
||||
return Err((
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
"E-post and/or XMPP must be supplied!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let claims = JWTClaims {
|
||||
sub: user.xmpp.clone().unwrap_or(user.epost.clone().unwrap()),
|
||||
exp: chrono::Utc::now().checked_add_signed(chrono::Duration::days(2)).expect("Valid timestamp").timestamp() as usize,
|
||||
};
|
||||
|
||||
let token = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &jsonwebtoken::EncodingKey::from_secret(std::env::var("JWT_SECRET").unwrap().as_bytes())).unwrap();
|
||||
let salt = argon2::password_hash::SaltString::generate(&mut rand_core::OsRng);
|
||||
let argon2 = argon2::Argon2::default();
|
||||
let hash = argon2
|
||||
.hash_password(user.password.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
sqlx::query_as!(
|
||||
User,
|
||||
r#"
|
||||
INSERT INTO users (id, name, epost, xmpp, site, password, active_token)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
"#,
|
||||
1,
|
||||
user.name,
|
||||
user.epost,
|
||||
user.xmpp,
|
||||
user.site,
|
||||
hash,
|
||||
token
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
|
||||
|
||||
return Ok(token);
|
||||
}
|
||||
return Err((axum::http::StatusCode::CONFLICT, "There is already a user in the database. We currently don't support more than one user.".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_feed() -> axum::response::Response {
|
||||
|
@ -17,22 +94,20 @@ async fn test_feed() -> axum::response::Response {
|
|||
atom_syndication::EntryBuilder::default()
|
||||
.title("First Test Post")
|
||||
.id("first-test-post".to_string())
|
||||
.authors(vec![
|
||||
atom_syndication::Person {
|
||||
.authors(vec![atom_syndication::Person {
|
||||
name: "Test Person".into(),
|
||||
email: Some("test@example.com".into()),
|
||||
uri: Some("http://localhost:3000".into()),
|
||||
..Default::default()
|
||||
}
|
||||
])
|
||||
}])
|
||||
.content(Some(
|
||||
atom_syndication::ContentBuilder::default()
|
||||
.lang(Some("en".into()))
|
||||
.value(Some("<p>This is a test post</p>".into()))
|
||||
.content_type(Some("html".into()))
|
||||
.build()
|
||||
.build(),
|
||||
))
|
||||
.build()
|
||||
.build(),
|
||||
);
|
||||
|
||||
let atom = atom_syndication::Feed {
|
||||
|
|
|
@ -1,2 +1,159 @@
|
|||
#![feature(fs_try_exists)]
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
fn main() {}
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Manipulate users (requires an initialised site)
|
||||
User {
|
||||
#[command(subcommand)]
|
||||
command: UserCommands,
|
||||
},
|
||||
Site {
|
||||
#[command(subcommand)]
|
||||
command: SiteCommands,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum UserCommands {
|
||||
/// Register a user with the site
|
||||
Register {
|
||||
/// The name to show on blog posts by this user
|
||||
#[arg(short, long)]
|
||||
name: String,
|
||||
/// The e-post/e-mail of this user, optional if XMPP address is supplied
|
||||
/// shows in the feed.
|
||||
#[arg(short, long)]
|
||||
epost: Option<String>,
|
||||
/// The XMPP address of the user, optional if e-post address is supplied
|
||||
/// does not show in the feed, but shows in the body as a contact.
|
||||
#[arg(short, long)]
|
||||
xmpp: Option<String>,
|
||||
/// The site URL for the user (should be the same as where the main blog shows)
|
||||
#[arg(short, long)]
|
||||
site: String,
|
||||
/// The password for the user
|
||||
#[arg(short, long)]
|
||||
password: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum SiteCommands {
|
||||
/// Initialises a site in the current directory
|
||||
Init {
|
||||
/// The name for the site
|
||||
#[arg(short, long)]
|
||||
name: String,
|
||||
/// The URL for the main site
|
||||
#[arg(short, long)]
|
||||
url: String,
|
||||
/// The URL for the API
|
||||
#[arg(short, long)]
|
||||
api: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct Config {
|
||||
site: SiteConfig,
|
||||
user: Option<UserConfig>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct SiteConfig {
|
||||
name: String,
|
||||
url: String,
|
||||
api_url: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct UserConfig {
|
||||
contact: String,
|
||||
jwt: String,
|
||||
}
|
||||
|
||||
fn deref_string_opt(val: &Option<String>) -> Option<String> {
|
||||
let v = match val {
|
||||
Some(s) => Some(s.to_string()),
|
||||
None => None
|
||||
};
|
||||
return v;
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Commands::User { command } => match command {
|
||||
UserCommands::Register { name, epost, xmpp, site, password } => {
|
||||
let cfg_file = std::fs::read_to_string("./blogrss.toml").unwrap();
|
||||
let cfg: Config = toml::from_str(&cfg_file).unwrap();
|
||||
if epost.is_none() && xmpp.is_none() {
|
||||
println!("E-post and/or xmpp must be supplied!");
|
||||
return;
|
||||
}
|
||||
|
||||
let new_user = blogrss::RegisterUser {
|
||||
name: name.to_string(),
|
||||
epost: deref_string_opt(epost),
|
||||
xmpp: deref_string_opt(xmpp),
|
||||
site: site.to_string(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = client.post(format!("{}/user/register", cfg.site.api_url))
|
||||
.json(&new_user)
|
||||
.send()
|
||||
.await.unwrap();
|
||||
|
||||
if res.status().is_success() {
|
||||
let new_cfg = Config {
|
||||
site: cfg.site,
|
||||
user: Some(UserConfig {
|
||||
jwt: res.text().await.unwrap(),
|
||||
contact: match deref_string_opt(xmpp) {
|
||||
Some(s) => format!("xmpp:{}", s),
|
||||
None => format!("epost:{}", deref_string_opt(epost).unwrap())
|
||||
}
|
||||
})
|
||||
};
|
||||
std::fs::write("./blogrss.toml", toml::to_string(&new_cfg).unwrap()).expect("Unable to write data");
|
||||
println!("Success! You are now registered and logged in");
|
||||
return;
|
||||
} else {
|
||||
eprintln!("There was an error creating your account");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Site { command } => match command {
|
||||
SiteCommands::Init { name, url, api } => {
|
||||
if std::fs::try_exists("./blogrss.toml").unwrap() {
|
||||
println!("Site already initialised!")
|
||||
} else {
|
||||
let cfg = Config {
|
||||
site: SiteConfig {
|
||||
api_url: api.to_string(),
|
||||
name: name.to_string(),
|
||||
url: url.to_string(),
|
||||
},
|
||||
user: None,
|
||||
};
|
||||
std::fs::write("./blogrss.toml", toml::to_string(&cfg).unwrap())
|
||||
.expect("Unable to write data");
|
||||
println!("Site initialised!")
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
28
src/lib.rs
28
src/lib.rs
|
@ -1,14 +1,20 @@
|
|||
pub fn add(left: usize, right: usize) -> usize {
|
||||
left + right
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, sqlx::FromRow)]
|
||||
pub struct RegisterUser {
|
||||
pub name: String,
|
||||
pub epost: Option<String>,
|
||||
pub xmpp: Option<String>,
|
||||
pub site: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
#[derive(serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub epost: Option<String>,
|
||||
pub xmpp: Option<String>,
|
||||
pub site: String,
|
||||
pub password: String,
|
||||
pub active_token: Option<String>
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue