initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
3143
Cargo.lock
generated
Normal file
3143
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal 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
28
src/database.rs
Normal 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
41
src/main.rs
Normal 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
57
src/models.rs
Normal 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
149
src/routes.rs
Normal 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
10
src/schema.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
id -> Integer,
|
||||
username -> Text,
|
||||
email -> Text,
|
||||
password_hash -> Text,
|
||||
created_at -> Text,
|
||||
updated_at -> Text,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user