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
|
/target
|
||||||
.envrc
|
.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"
|
name = "blogrss"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
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]
|
[lib]
|
||||||
|
|
||||||
|
@ -14,9 +19,18 @@ name = "blogrss-server"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
argon2 = "0.5.2"
|
||||||
atom_syndication = { version = "0.12.2", features = ["with-serde"] }
|
atom_syndication = { version = "0.12.2", features = ["with-serde"] }
|
||||||
axum = "0.7.3"
|
axum = "0.7.3"
|
||||||
axum-xml = "0.2.0"
|
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"] }
|
rss = { version = "2.0.6", features = ["serde", "atom", "atom_syndication", "chrono", "with-serde"] }
|
||||||
|
rustcrypto = "0.0.0"
|
||||||
serde = { version = "1.0.195", features = ["derive"] }
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.7.3", features = ["runtime-tokio", "sqlite"] }
|
||||||
tokio = { version = "1.35.1", features = ["full"] }
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let app = axum::Router::new()
|
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
.route("/", axum::routing::get(test_feed));
|
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
|
.await
|
||||||
.unwrap();
|
.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 {
|
async fn test_feed() -> axum::response::Response {
|
||||||
|
@ -17,22 +94,20 @@ async fn test_feed() -> axum::response::Response {
|
||||||
atom_syndication::EntryBuilder::default()
|
atom_syndication::EntryBuilder::default()
|
||||||
.title("First Test Post")
|
.title("First Test Post")
|
||||||
.id("first-test-post".to_string())
|
.id("first-test-post".to_string())
|
||||||
.authors(vec![
|
.authors(vec![atom_syndication::Person {
|
||||||
atom_syndication::Person {
|
|
||||||
name: "Test Person".into(),
|
name: "Test Person".into(),
|
||||||
email: Some("test@example.com".into()),
|
email: Some("test@example.com".into()),
|
||||||
uri: Some("http://localhost:3000".into()),
|
uri: Some("http://localhost:3000".into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}])
|
||||||
])
|
|
||||||
.content(Some(
|
.content(Some(
|
||||||
atom_syndication::ContentBuilder::default()
|
atom_syndication::ContentBuilder::default()
|
||||||
.lang(Some("en".into()))
|
.lang(Some("en".into()))
|
||||||
.value(Some("<p>This is a test post</p>".into()))
|
.value(Some("<p>This is a test post</p>".into()))
|
||||||
.content_type(Some("html".into()))
|
.content_type(Some("html".into()))
|
||||||
.build()
|
.build(),
|
||||||
))
|
))
|
||||||
.build()
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let atom = atom_syndication::Feed {
|
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)]
|
#[derive(serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||||
mod tests {
|
pub struct User {
|
||||||
use super::*;
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
#[test]
|
pub epost: Option<String>,
|
||||||
fn it_works() {
|
pub xmpp: Option<String>,
|
||||||
let result = add(2, 2);
|
pub site: String,
|
||||||
assert_eq!(result, 4);
|
pub password: String,
|
||||||
}
|
pub active_token: Option<String>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue