initial commit

This commit is contained in:
2026-03-13 12:50:45 -04:00
commit 10ccdacc0d
9 changed files with 3445 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

3143
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "user_service"
version = "0.1.0"
edition = "2021"
[dependencies]
env_logger = "0.11"
actix-web = "4.9"
actix-rt = "2.10"
diesel = { version = "2.2", features = ["sqlite", "r2d2"] }
diesel_migrations = "2.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio"] }
tokio = { version = "1.0", features = ["full"] }
thiserror = "1.0"

28
src/database.rs Normal file
View File

@@ -0,0 +1,28 @@
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
pub type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
pub fn establish_connection(database_url: &str) -> DbPool {
let manager = ConnectionManager::<SqliteConnection>::new(database_url);
r2d2::Pool::builder()
.max_size(10)
.build(manager)
.expect("Failed to create pool")
}
pub fn run_migrations(conn: &mut SqliteConnection) -> Result<(), diesel::result::Error> {
// Create users table if it doesn't exist
diesel::sql_query(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
).execute(conn)?;
Ok(())
}

41
src/main.rs Normal file
View File

@@ -0,0 +1,41 @@
mod database;
mod models;
mod routes;
mod schema;
use actix_web::{App, HttpServer, web, HttpResponse, http::StatusCode};
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
use diesel::sqlite::SqliteConnection;
use std::env;
type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info,user_service=info");
env_logger::init();
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "users.db".to_string());
let manager = ConnectionManager::<SqliteConnection>::new(database_url);
let pool = r2d2::Pool::builder()
.max_size(10)
.build(manager)
.expect("Failed to create pool");
// Run migrations - need mutable reference from pooled connection
database::run_migrations(&mut *pool.get().expect("Failed to get connection"))
.expect("Failed to run migrations");
let pool_data = web::Data::new(pool.clone());
HttpServer::new(move || {
App::new()
.app_data(pool_data.clone())
.configure(routes::configure_routes)
})
.bind(("127.0.0.1", 8923))?
.run()
.await
}

57
src/models.rs Normal file
View File

@@ -0,0 +1,57 @@
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use crate::schema::users;
#[derive(Debug, Clone, Serialize, Deserialize, Queryable)]
pub struct User {
pub id: i32,
pub username: String,
pub email: String,
pub password_hash: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Insertable)]
#[diesel(table_name = users)]
pub struct NewUser {
pub username: String,
pub email: String,
pub password_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUser {
pub username: String,
pub email: String,
pub password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUser {
pub username: Option<String>,
pub email: Option<String>,
pub password: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserResponse {
pub id: i32,
pub username: String,
pub email: String,
}
impl From<User> for UserResponse {
fn from(user: User) -> Self {
Self {
id: user.id,
username: user.username,
email: user.email,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsersList {
pub users: Vec<UserResponse>,
}

149
src/routes.rs Normal file
View File

@@ -0,0 +1,149 @@
use actix_web::{web, HttpResponse, Error};
use actix_web::http::StatusCode;
use actix_web::error::{ErrorInternalServerError, ErrorNotFound, ErrorConflict};
use diesel::prelude::*;
use diesel::dsl::sql;
use crate::database::DbPool;
use crate::models::{User, NewUser, CreateUser, UpdateUser, UserResponse, UsersList};
use crate::schema::users;
// Get all users
pub async fn get_users(pool: web::Data<DbPool>) -> Result<HttpResponse, Error> {
let mut conn = pool.get().map_err(|_| ErrorInternalServerError("Database connection error"))?;
let result = users::table.load::<User>(&mut conn).map_err(|e| {
eprintln!("Error loading users: {:?}", e);
ErrorInternalServerError("Failed to load users")
})?;
let user_responses: Vec<UserResponse> = result.iter().map(|u| UserResponse::from(u.clone())).collect();
Ok(HttpResponse::Ok().json(UsersList { users: user_responses }))
}
// Get user by ID
pub async fn get_user(pool: web::Data<DbPool>, path: web::Path<i32>) -> Result<HttpResponse, Error> {
let user_id = path.into_inner();
let mut conn = pool.get().map_err(|_| ErrorInternalServerError("Database connection error"))?;
let user = users::table.find(user_id).first::<User>(&mut conn).map_err(|e| {
if e.to_string().contains("not found") {
ErrorNotFound("User not found")
} else {
eprintln!("Error loading user: {:?}", e);
ErrorInternalServerError("Failed to load user")
}
})?;
Ok(HttpResponse::Ok().json(UserResponse::from(user)))
}
// Create new user
pub async fn create_user(
pool: web::Data<DbPool>,
user: web::Json<CreateUser>,
) -> Result<HttpResponse, Error> {
let new_user = user.into_inner();
let mut conn = pool.get().map_err(|_| ErrorInternalServerError("Database connection error"))?;
// Hash password (using a simple placeholder - in production use bcrypt)
let password_hash = format!("hashed_{}", new_user.password);
let new_user_entry = NewUser {
username: new_user.username,
email: new_user.email,
password_hash,
};
diesel::insert_into(users::table)
.values(&new_user_entry)
.execute(&mut conn)
.map_err(|e| {
eprintln!("Error creating user: {:?}", e);
if e.to_string().contains("UNIQUE constraint failed") {
ErrorConflict("Username or email already exists")
} else {
ErrorInternalServerError("Failed to create user")
}
})?;
Ok(HttpResponse::Created().json("User created successfully"))
}
// Update user
pub async fn update_user(
pool: web::Data<DbPool>,
path: web::Path<i32>,
user: web::Json<UpdateUser>,
) -> Result<HttpResponse, Error> {
let user_id = path.into_inner();
let update_data = user.into_inner();
let mut conn = pool.get().map_err(|_| ErrorInternalServerError("Database connection error"))?;
// Check if user exists
let existing_user = match users::table.find(user_id).first::<User>(&mut conn) {
Ok(u) => u,
Err(e) => {
if e.to_string().contains("not found") {
return Err(ErrorNotFound("User not found"));
}
return Err(ErrorInternalServerError("Failed to load user"));
}
};
let username = update_data.username.unwrap_or(existing_user.username);
let email = update_data.email.unwrap_or(existing_user.email);
let password_hash = update_data.password
.map(|p| format!("hashed_{}", p))
.unwrap_or(existing_user.password_hash);
diesel::update(users::table.find(user_id))
.set((
users::username.eq(&username),
users::email.eq(&email),
users::password_hash.eq(&password_hash),
users::updated_at.eq(sql("datetime('now')")),
))
.execute(&mut conn)
.map_err(|e| {
eprintln!("Error updating user: {:?}", e);
if e.to_string().contains("UNIQUE constraint failed") {
ErrorConflict("Username or email already exists")
} else {
ErrorInternalServerError("Failed to update user")
}
})?;
Ok(HttpResponse::Ok().json("User updated successfully"))
}
// Delete user
pub async fn delete_user(pool: web::Data<DbPool>, path: web::Path<i32>) -> Result<HttpResponse, Error> {
let user_id = path.into_inner();
let mut conn = pool.get().map_err(|_| ErrorInternalServerError("Database connection error"))?;
let rows_deleted = diesel::delete(users::table.find(user_id))
.execute(&mut conn)
.map_err(|e| {
eprintln!("Error deleting user: {:?}", e);
ErrorInternalServerError("Failed to delete user")
})?;
if rows_deleted == 0 {
return Err(ErrorNotFound("User not found"));
}
Ok(HttpResponse::Ok().json("User deleted successfully"))
}
// Configure routes
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/api/users")
.route("", web::get().to(get_users))
.route("", web::post().to(create_user))
.route("/{id}", web::get().to(get_user))
.route("/{id}", web::put().to(update_user))
.route("/{id}", web::delete().to(delete_user))
);
}

10
src/schema.rs Normal file
View File

@@ -0,0 +1,10 @@
diesel::table! {
users (id) {
id -> Integer,
username -> Text,
email -> Text,
password_hash -> Text,
created_at -> Text,
updated_at -> Text,
}
}

BIN
users.db Normal file

Binary file not shown.