Compare commits
4 Commits
5f3a9473ff
...
86d49c56dc
Author | SHA1 | Date | |
---|---|---|---|
|
86d49c56dc | ||
|
50bfe09bcf | ||
|
6e815a94a0 | ||
|
c74ebbe5fa |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2663,10 +2663,12 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"atom_syndication",
|
"atom_syndication",
|
||||||
|
"base64 0.21.7",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"feed-rs",
|
"feed-rs",
|
||||||
|
"getrandom 0.2.15",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_db_pools",
|
"rocket_db_pools",
|
||||||
|
@ -20,3 +20,5 @@ feed-rs = "2.3.1"
|
|||||||
reqwest = { version = "0.12.12", features = ["json"] }
|
reqwest = { version = "0.12.12", features = ["json"] }
|
||||||
tokio = "1.43.0"
|
tokio = "1.43.0"
|
||||||
time = "0.3.37"
|
time = "0.3.37"
|
||||||
|
base64 = "0.21"
|
||||||
|
getrandom = "0.2"
|
||||||
|
14
TODO.md
14
TODO.md
@ -1,24 +1,10 @@
|
|||||||
# TODO List
|
# TODO List
|
||||||
|
|
||||||
## Security Improvements
|
|
||||||
|
|
||||||
### Make Server Secret Configurable
|
|
||||||
Currently, the server secret used for cookie encryption is not configurable and uses Rocket's default. We should:
|
|
||||||
- Add a configuration option for the server secret
|
|
||||||
- Allow it to be set via environment variable or config file
|
|
||||||
- Generate and persist a random secret on first run if none is provided
|
|
||||||
- Add documentation about the security implications of the secret
|
|
||||||
|
|
||||||
### Improve Session Management
|
### Improve Session Management
|
||||||
Current session management is basic and needs improvement:
|
Current session management is basic and needs improvement:
|
||||||
- Replace simple user_id cookie with a proper session system
|
|
||||||
- Add session expiry and renewal logic
|
- Add session expiry and renewal logic
|
||||||
- Store sessions in the database with proper cleanup
|
- Store sessions in the database with proper cleanup
|
||||||
- Add ability to revoke sessions
|
- Add ability to revoke sessions
|
||||||
- Consider adding "remember me" functionality
|
- Consider adding "remember me" functionality
|
||||||
- Add session tracking (last used, IP, user agent, etc.)
|
- Add session tracking (last used, IP, user agent, etc.)
|
||||||
|
|
||||||
Reference: [Current basic implementation in user.rs](src/user.rs) with the comment:
|
|
||||||
```rust
|
|
||||||
// TODO there should be a more complicated notion of a session
|
|
||||||
```
|
|
79
src/demo.rs
79
src/demo.rs
@ -1,52 +1,33 @@
|
|||||||
use chrono;
|
|
||||||
use sqlx;
|
|
||||||
use rocket::serde;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::feeds::Feed;
|
use crate::feeds::Feed;
|
||||||
|
use crate::user::User;
|
||||||
|
|
||||||
pub async fn setup_demo_data(pool: &sqlx::SqlitePool) {
|
pub async fn setup_demo_data(pool: &sqlx::SqlitePool) {
|
||||||
// Create admin user
|
// Create admin user
|
||||||
let admin_id = Uuid::new_v4();
|
let mut admin = User::new(
|
||||||
let admin_id_str = admin_id.to_string();
|
"admin".to_string(),
|
||||||
let admin_hash = bcrypt::hash("admin", bcrypt::DEFAULT_COST).unwrap();
|
bcrypt::hash("admin", bcrypt::DEFAULT_COST).unwrap(),
|
||||||
let now = chrono::Utc::now().to_rfc3339();
|
None,
|
||||||
|
None,
|
||||||
sqlx::query(
|
);
|
||||||
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin)
|
admin.admin = true;
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
admin
|
||||||
)
|
.write_to_database(pool)
|
||||||
.bind(&admin_id_str)
|
|
||||||
.bind("admin")
|
|
||||||
.bind(&admin_hash)
|
|
||||||
.bind(Option::<String>::None)
|
|
||||||
.bind(Option::<String>::None)
|
|
||||||
.bind(&now)
|
|
||||||
.bind(true)
|
|
||||||
.execute(pool)
|
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create admin user");
|
.expect("Failed to create admin user");
|
||||||
|
|
||||||
// Create demo user
|
// Create demo user
|
||||||
let demo_id = Uuid::new_v4();
|
let demo = User::new(
|
||||||
let demo_id_str = demo_id.to_string();
|
"demo".to_string(),
|
||||||
let demo_hash = bcrypt::hash("demo", bcrypt::DEFAULT_COST).unwrap();
|
bcrypt::hash("demo", bcrypt::DEFAULT_COST).unwrap(),
|
||||||
|
None,
|
||||||
sqlx::query(
|
None,
|
||||||
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin)
|
);
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
demo.write_to_database(pool)
|
||||||
)
|
|
||||||
.bind(&demo_id_str)
|
|
||||||
.bind("demo")
|
|
||||||
.bind(&demo_hash)
|
|
||||||
.bind(Option::<String>::None)
|
|
||||||
.bind(Option::<String>::None)
|
|
||||||
.bind(&now)
|
|
||||||
.bind(false)
|
|
||||||
.execute(pool)
|
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create demo user");
|
.expect("Failed to create demo user");
|
||||||
|
|
||||||
|
let demo_id = demo.id;
|
||||||
|
|
||||||
let mut bbc_news = Feed::new(
|
let mut bbc_news = Feed::new(
|
||||||
"BBC News".to_string(),
|
"BBC News".to_string(),
|
||||||
"https://feeds.bbci.co.uk/news/world/us_and_canada/rss.xml"
|
"https://feeds.bbci.co.uk/news/world/us_and_canada/rss.xml"
|
||||||
@ -77,30 +58,10 @@ pub async fn setup_demo_data(pool: &sqlx::SqlitePool) {
|
|||||||
);
|
);
|
||||||
acx.categorization = vec!["Substack".to_string()];
|
acx.categorization = vec!["Substack".to_string()];
|
||||||
|
|
||||||
|
|
||||||
let feeds = [bbc_news, xkcd, isidore, acx];
|
let feeds = [bbc_news, xkcd, isidore, acx];
|
||||||
|
|
||||||
for feed in feeds {
|
for feed in feeds {
|
||||||
// TODO: This insert logic is substantially the same as Feed::write_to_database.
|
feed.write_to_database(pool)
|
||||||
// Should find a way to unify these two code paths to avoid duplication.
|
|
||||||
let categorization_json = serde::json::to_value(feed.categorization).map_err(|e| {
|
|
||||||
eprintln!("Failed to serialize categorization: {}", e);
|
|
||||||
sqlx::Error::Decode(Box::new(e))
|
|
||||||
}).unwrap();
|
|
||||||
println!("{}", categorization_json);
|
|
||||||
|
|
||||||
sqlx::query(
|
|
||||||
"INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, json(?7))",
|
|
||||||
)
|
|
||||||
.bind(feed.feed_id.to_string())
|
|
||||||
.bind(&feed.name)
|
|
||||||
.bind(feed.url.as_str())
|
|
||||||
.bind(feed.user_id.to_string())
|
|
||||||
.bind(feed.added_time.to_rfc3339())
|
|
||||||
.bind(feed.last_checked_time.to_rfc3339())
|
|
||||||
.bind(categorization_json.to_string())
|
|
||||||
.execute(pool)
|
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create demo feed");
|
.expect("Failed to create demo feed");
|
||||||
}
|
}
|
||||||
|
12
src/feeds.rs
12
src/feeds.rs
@ -2,6 +2,7 @@ use rocket::http::Status;
|
|||||||
use rocket::serde::{self, json::Json, Deserialize, Serialize};
|
use rocket::serde::{self, json::Json, Deserialize, Serialize};
|
||||||
use rocket_db_pools::Connection;
|
use rocket_db_pools::Connection;
|
||||||
use sqlx::types::JsonValue;
|
use sqlx::types::JsonValue;
|
||||||
|
use sqlx::Executor;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -35,7 +36,10 @@ impl Feed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn write_to_database(&self, mut db: Connection<Db>) -> Result<(), sqlx::Error> {
|
pub async fn write_to_database<'a, E>(&self, executor: E) -> sqlx::Result<()>
|
||||||
|
where
|
||||||
|
E: Executor<'a, Database = sqlx::Sqlite>,
|
||||||
|
{
|
||||||
// Convert categorization to JSON value
|
// Convert categorization to JSON value
|
||||||
let categorization_json = serde::json::to_value(&self.categorization).map_err(|e| {
|
let categorization_json = serde::json::to_value(&self.categorization).map_err(|e| {
|
||||||
eprintln!("Failed to serialize categorization: {}", e);
|
eprintln!("Failed to serialize categorization: {}", e);
|
||||||
@ -53,7 +57,7 @@ impl Feed {
|
|||||||
.bind(self.added_time.to_rfc3339())
|
.bind(self.added_time.to_rfc3339())
|
||||||
.bind(self.last_checked_time.to_rfc3339())
|
.bind(self.last_checked_time.to_rfc3339())
|
||||||
.bind(categorization_json.to_string())
|
.bind(categorization_json.to_string())
|
||||||
.execute(&mut **db)
|
.execute(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -70,7 +74,7 @@ pub struct NewFeed {
|
|||||||
|
|
||||||
#[post("/feeds", data = "<new_feed>")]
|
#[post("/feeds", data = "<new_feed>")]
|
||||||
pub async fn create_feed(
|
pub async fn create_feed(
|
||||||
db: Connection<Db>,
|
mut db: Connection<Db>,
|
||||||
new_feed: Json<NewFeed>,
|
new_feed: Json<NewFeed>,
|
||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
) -> Result<Json<Feed>, Status> {
|
) -> Result<Json<Feed>, Status> {
|
||||||
@ -97,7 +101,7 @@ pub async fn create_feed(
|
|||||||
let mut feed = Feed::new(name, new_feed.url, user.user_id);
|
let mut feed = Feed::new(name, new_feed.url, user.user_id);
|
||||||
feed.categorization = new_feed.categorization;
|
feed.categorization = new_feed.categorization;
|
||||||
|
|
||||||
match feed.write_to_database(db).await {
|
match feed.write_to_database(&mut **db).await {
|
||||||
Ok(_) => Ok(Json(feed)),
|
Ok(_) => Ok(Json(feed)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Database error: {}", e);
|
eprintln!("Database error: {}", e);
|
||||||
|
@ -101,7 +101,10 @@ fn rocket() -> _ {
|
|||||||
|
|
||||||
let figment = rocket::Config::figment()
|
let figment = rocket::Config::figment()
|
||||||
.merge(("databases.rss_data.url", db_url))
|
.merge(("databases.rss_data.url", db_url))
|
||||||
.merge(("secret_key", std::env::var("SECRET_KEY").expect("SECRET_KEY environment variable must be set")));
|
.merge((
|
||||||
|
"secret_key",
|
||||||
|
std::env::var("SECRET_KEY").expect("SECRET_KEY environment variable must be set"),
|
||||||
|
));
|
||||||
|
|
||||||
rocket::custom(figment)
|
rocket::custom(figment)
|
||||||
.mount(
|
.mount(
|
||||||
@ -128,6 +131,7 @@ fn rocket() -> _ {
|
|||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
.attach(Db::init())
|
.attach(Db::init())
|
||||||
.manage(args.demo)
|
.manage(args.demo)
|
||||||
|
.manage(user::SessionStore::new())
|
||||||
.attach(AdHoc::try_on_ignite("DB Setup", move |rocket| async move {
|
.attach(AdHoc::try_on_ignite("DB Setup", move |rocket| async move {
|
||||||
setup_database(args.demo, rocket).await
|
setup_database(args.demo, rocket).await
|
||||||
}))
|
}))
|
||||||
|
147
src/user.rs
147
src/user.rs
@ -1,13 +1,57 @@
|
|||||||
use time::Duration;
|
use time::Duration;
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||||
use rocket::http::{Cookie, CookieJar, Status};
|
use rocket::http::{Cookie, CookieJar, Status};
|
||||||
use rocket::serde::{json::Json, Deserialize, Serialize};
|
use rocket::serde::{json::Json, Deserialize, Serialize};
|
||||||
|
use rocket::State;
|
||||||
use rocket_db_pools::Connection;
|
use rocket_db_pools::Connection;
|
||||||
use rocket_dyn_templates::{context, Template};
|
use rocket_dyn_templates::{context, Template};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
|
|
||||||
|
pub struct SessionStore(RwLock<HashMap<Uuid, HashSet<String>>>);
|
||||||
|
|
||||||
|
impl SessionStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
SessionStore(RwLock::new(HashMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_secret() -> String {
|
||||||
|
let mut bytes = [0u8; 32];
|
||||||
|
getrandom::getrandom(&mut bytes).expect("Failed to generate random bytes");
|
||||||
|
BASE64.encode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self, user_id: Uuid, secret: String) {
|
||||||
|
let mut store = self.0.write().unwrap();
|
||||||
|
store
|
||||||
|
.entry(user_id)
|
||||||
|
.or_insert_with(HashSet::new)
|
||||||
|
.insert(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(&self, user_id: Uuid, secret: &str) -> bool {
|
||||||
|
let store = self.0.read().unwrap();
|
||||||
|
store
|
||||||
|
.get(&user_id)
|
||||||
|
.map_or(false, |secrets| secrets.contains(secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&self, user_id: Uuid, secret: &str) {
|
||||||
|
let mut store = self.0.write().unwrap();
|
||||||
|
if let Some(secrets) = store.get_mut(&user_id) {
|
||||||
|
secrets.remove(secret);
|
||||||
|
// Clean up the user entry if no sessions remain
|
||||||
|
if secrets.is_empty() {
|
||||||
|
store.remove(&user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
@ -37,6 +81,29 @@ impl User {
|
|||||||
admin: false,
|
admin: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn write_to_database<'a, E>(&self, executor: E) -> sqlx::Result<()>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::Sqlite>,
|
||||||
|
{
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(self.id.to_string())
|
||||||
|
.bind(self.username.clone())
|
||||||
|
.bind(self.password_hash.clone())
|
||||||
|
.bind(self.email.clone())
|
||||||
|
.bind(self.display_name.clone())
|
||||||
|
.bind(self.created_at.to_rfc3339())
|
||||||
|
.bind(self.admin)
|
||||||
|
.execute(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@ -86,19 +153,7 @@ pub async fn create_user(
|
|||||||
new_user.display_name,
|
new_user.display_name,
|
||||||
);
|
);
|
||||||
|
|
||||||
let query = sqlx::query(
|
match user.write_to_database(&mut **db).await {
|
||||||
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
|
|
||||||
)
|
|
||||||
.bind(user.id.to_string())
|
|
||||||
.bind(user.username.as_str())
|
|
||||||
.bind(user.password_hash.as_str())
|
|
||||||
.bind(user.email.as_ref())
|
|
||||||
.bind(user.display_name.as_ref())
|
|
||||||
.bind(user.created_at.to_rfc3339())
|
|
||||||
.bind(false)
|
|
||||||
.execute(&mut **db).await;
|
|
||||||
|
|
||||||
match query {
|
|
||||||
Ok(_) => Ok(Json(user)),
|
Ok(_) => Ok(Json(user)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Database error: {}", e);
|
eprintln!("Database error: {}", e);
|
||||||
@ -184,6 +239,7 @@ pub async fn login(
|
|||||||
mut db: Connection<Db>,
|
mut db: Connection<Db>,
|
||||||
credentials: Json<LoginCredentials>,
|
credentials: Json<LoginCredentials>,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
|
sessions: &State<SessionStore>,
|
||||||
) -> Result<Json<LoginResponse>, Status> {
|
) -> Result<Json<LoginResponse>, Status> {
|
||||||
let creds = credentials.into_inner();
|
let creds = credentials.into_inner();
|
||||||
|
|
||||||
@ -219,13 +275,15 @@ pub async fn login(
|
|||||||
return Err(Status::Unauthorized);
|
return Err(Status::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set session cookie
|
// Generate and store session
|
||||||
// TODO tehre should be a more complicated notion of a session
|
|
||||||
let user_id = Uuid::parse_str(&user.id).map_err(|_| Status::InternalServerError)?;
|
let user_id = Uuid::parse_str(&user.id).map_err(|_| Status::InternalServerError)?;
|
||||||
|
let session_secret = SessionStore::generate_secret();
|
||||||
|
sessions.store(user_id, session_secret.clone());
|
||||||
|
|
||||||
//TODO make this user-configurable
|
// Set session cookie with both user_id and secret
|
||||||
let max_age = Duration::days(6);
|
let max_age = Duration::days(6);
|
||||||
let mut cookie = Cookie::new("user_id", user_id.to_string());
|
let cookie_value = format!("{}:{}", user_id, session_secret);
|
||||||
|
let mut cookie = Cookie::new("user_id", cookie_value);
|
||||||
cookie.set_max_age(max_age);
|
cookie.set_max_age(max_age);
|
||||||
cookies.add_private(cookie);
|
cookies.add_private(cookie);
|
||||||
|
|
||||||
@ -236,8 +294,15 @@ pub async fn login(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/logout")]
|
#[post("/logout")]
|
||||||
pub fn logout(cookies: &CookieJar<'_>) -> Status {
|
pub fn logout(cookies: &CookieJar<'_>, sessions: &State<SessionStore>) -> Status {
|
||||||
|
if let Some(cookie) = cookies.get_private("user_id") {
|
||||||
|
if let Some((user_id, secret)) = cookie.value().split_once(':') {
|
||||||
|
if let Ok(user_id) = Uuid::parse_str(user_id) {
|
||||||
|
sessions.remove(user_id, secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
cookies.remove_private(Cookie::build("user_id"));
|
cookies.remove_private(Cookie::build("user_id"));
|
||||||
|
}
|
||||||
Status::NoContent
|
Status::NoContent
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,14 +320,19 @@ impl<'r> rocket::request::FromRequest<'r> for AuthenticatedUser {
|
|||||||
) -> rocket::request::Outcome<Self, Self::Error> {
|
) -> rocket::request::Outcome<Self, Self::Error> {
|
||||||
use rocket::request::Outcome;
|
use rocket::request::Outcome;
|
||||||
|
|
||||||
|
let sessions = request.rocket().state::<SessionStore>().unwrap();
|
||||||
|
|
||||||
match request.cookies().get_private("user_id") {
|
match request.cookies().get_private("user_id") {
|
||||||
Some(cookie) => {
|
Some(cookie) => {
|
||||||
if let Ok(user_id) = Uuid::parse_str(cookie.value()) {
|
if let Some((user_id, secret)) = cookie.value().split_once(':') {
|
||||||
Outcome::Success(AuthenticatedUser { user_id })
|
if let Ok(user_id) = Uuid::parse_str(user_id) {
|
||||||
} else {
|
if sessions.verify(user_id, secret) {
|
||||||
Outcome::Forward(Status::Unauthorized)
|
return Outcome::Success(AuthenticatedUser { user_id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Outcome::Forward(Status::Unauthorized)
|
||||||
|
}
|
||||||
None => Outcome::Forward(Status::Unauthorized),
|
None => Outcome::Forward(Status::Unauthorized),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -291,6 +361,8 @@ pub async fn setup(
|
|||||||
mut db: Connection<Db>,
|
mut db: Connection<Db>,
|
||||||
new_user: Json<NewUser>,
|
new_user: Json<NewUser>,
|
||||||
) -> Result<Status, Json<SetupError>> {
|
) -> Result<Status, Json<SetupError>> {
|
||||||
|
let new_user = new_user.into_inner();
|
||||||
|
|
||||||
// Check if any users exist
|
// Check if any users exist
|
||||||
let count = sqlx::query!("SELECT COUNT(*) as count FROM users")
|
let count = sqlx::query!("SELECT COUNT(*) as count FROM users")
|
||||||
.fetch_one(&mut **db)
|
.fetch_one(&mut **db)
|
||||||
@ -309,34 +381,25 @@ pub async fn setup(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let password = new_user.password.as_bytes();
|
||||||
|
|
||||||
// Hash the password
|
// Hash the password
|
||||||
let password_hash =
|
let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST).map_err(|e| {
|
||||||
bcrypt::hash(new_user.password.as_bytes(), bcrypt::DEFAULT_COST).map_err(|e| {
|
|
||||||
eprintln!("Password hashing error: {}", e);
|
eprintln!("Password hashing error: {}", e);
|
||||||
Json(SetupError {
|
Json(SetupError {
|
||||||
error: "Failed to process password".to_string(),
|
error: "Failed to process password".to_string(),
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Create admin user
|
let mut user = User::new(
|
||||||
let user_id = Uuid::new_v4().to_string();
|
new_user.username,
|
||||||
let now = chrono::Utc::now().to_rfc3339();
|
password_hash,
|
||||||
|
new_user.email,
|
||||||
|
new_user.display_name,
|
||||||
|
);
|
||||||
|
user.admin = true; // This is an admin user
|
||||||
|
|
||||||
let result = sqlx::query(
|
match user.write_to_database(&mut **db).await {
|
||||||
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
|
||||||
)
|
|
||||||
.bind(&user_id)
|
|
||||||
.bind(&new_user.username)
|
|
||||||
.bind(&password_hash)
|
|
||||||
.bind(&new_user.email)
|
|
||||||
.bind(&new_user.display_name)
|
|
||||||
.bind(&now)
|
|
||||||
.bind(true) // This is an admin user
|
|
||||||
.execute(&mut **db)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => Ok(Status::Created),
|
Ok(_) => Ok(Status::Created),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Database error: {}", e);
|
eprintln!("Database error: {}", e);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user