diff --git a/django_project/django_project/urls.py b/django_project/django_project/urls.py index 55db62141d94114859a66bccdf1978cfc847cd63..03e8563781e11a71dda1e3da7ad511d65c2b0bd4 100644 --- a/django_project/django_project/urls.py +++ b/django_project/django_project/urls.py @@ -25,4 +25,8 @@ urlpatterns = [ path('account/', views.account_view, name='account'), path('delete/', views.delete_account_view, name='delete_account'), path('logout/', views.logout_view, name='logout'), + path('groups/', views.group_list_view, name='group_list'), + path('groups/create/', views.group_create_view, name='group_create'), + path('groups/<int:group_id>/update/', views.group_update_view, name='group_update'), + path('groups/<int:group_id>/delete/', views.group_delete_view, name='group_delete'), ] diff --git a/django_project/myapp/templates/myapp/group_create.html b/django_project/myapp/templates/myapp/group_create.html new file mode 100644 index 0000000000000000000000000000000000000000..d1a52b690eb98f1b616f4e5e6f92e5a734312b44 --- /dev/null +++ b/django_project/myapp/templates/myapp/group_create.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Create Group</title> +</head> +<body> + <h1>Create Group</h1> + {% if error %} + <p style="color: red;">{{ error }}</p> + {% endif %} + <form method="post"> + {% csrf_token %} + <label for="id_name">Group Name:</label> + <input type="text" name="name" id="id_name" required> + <br><br> + <button type="submit">Create Group</button> + </form> + <p><a href="{% url 'group_list' %}">Back to Group List</a></p> +</body> +</html> + diff --git a/django_project/myapp/templates/myapp/group_delete.html b/django_project/myapp/templates/myapp/group_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..2d6dfeeec6a573c8abdb33cea26ccd6d43c885c3 --- /dev/null +++ b/django_project/myapp/templates/myapp/group_delete.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Delete Group</title> +</head> +<body> + <h1>Delete Group</h1> + {% if error %} + <p style="color: red;">{{ error }}</p> + {% endif %} + {% if group %} + <p>Are you sure you want to delete the group "{{ group.name }}"?</p> + <form method="post"> + {% csrf_token %} + <button type="submit">Yes, delete group</button> + </form> + {% else %} + <p>Group not found.</p> + {% endif %} + <p><a href="{% url 'group_list' %}">Back to Group List</a></p> +</body> +</html> + diff --git a/django_project/myapp/templates/myapp/group_list.html b/django_project/myapp/templates/myapp/group_list.html new file mode 100644 index 0000000000000000000000000000000000000000..1c5ee84cfac799186bf595191e51b23401d5f14a --- /dev/null +++ b/django_project/myapp/templates/myapp/group_list.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Group List</title> +</head> +<body> + <h1>Groups</h1> + {% if error %} + <p style="color: red;">{{ error }}</p> + {% endif %} + <ul> + {% for group in groups %} + <li> + {{ group.name }} + <a href="{% url 'group_update' group.id %}">Edit</a> + <a href="{% url 'group_delete' group.id %}">Delete</a> + </li> + {% empty %} + <li>No groups available.</li> + {% endfor %} + </ul> + <p><a href="{% url 'group_create' %}">Create New Group</a></p> +</body> +</html> + diff --git a/django_project/myapp/templates/myapp/group_update.html b/django_project/myapp/templates/myapp/group_update.html new file mode 100644 index 0000000000000000000000000000000000000000..1e9945ab0a31e6cbc7cd9cd4a12115e491607452 --- /dev/null +++ b/django_project/myapp/templates/myapp/group_update.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Update Group</title> +</head> +<body> + <h1>Update Group</h1> + {% if error %} + <p style="color: red;">{{ error }}</p> + {% endif %} + {% if group %} + <form method="post"> + {% csrf_token %} + <label for="id_name">New Group Name:</label> + <input type="text" name="name" id="id_name" value="{{ group.name }}" required> + <br><br> + <button type="submit">Update Group</button> + </form> + {% else %} + <p>Group not found.</p> + {% endif %} + <p><a href="{% url 'group_list' %}">Back to Group List</a></p> +</body> +</html> + diff --git a/django_project/myapp/views.py b/django_project/myapp/views.py index 24e97ca27558b9293de34be037ced58ec9236088..5bf7b2825ea0542ae8981db4edf47c2c3c9416f5 100644 --- a/django_project/myapp/views.py +++ b/django_project/myapp/views.py @@ -130,3 +130,89 @@ def logout_view(request): # Optionally, render a confirmation page or redirect to login. return render(request, 'myapp/logout.html') +def group_list_view(request): + """ + List all groups. + """ + db_url = settings.DATABASE_URL + try: + groups = rust_crud_api.get_all_groups(db_url) + except Exception as e: + return render(request, 'myapp/group_list.html', {'error': str(e)}) + return render(request, 'myapp/group_list.html', {'groups': groups}) + +def group_create_view(request): + """ + Create a new group. + """ + db_url = settings.DATABASE_URL + context = {} + if request.method == 'POST': + group_name = request.POST.get('name', '').strip() + if not group_name: + context['error'] = "Group name is required." + return render(request, 'myapp/group_create.html', context) + try: + rust_crud_api.create_group(db_url, group_name) + return redirect('group_list') + except Exception as e: + context['error'] = f"An error occurred: {e}" + return render(request, 'myapp/group_create.html', context) + return render(request, 'myapp/group_create.html', context) + +def group_update_view(request, group_id): + """ + Update an existing group's name. + """ + db_url = settings.DATABASE_URL + context = {} + try: + group = rust_crud_api.get_group(db_url, int(group_id)) + if group is None: + context['error'] = "Group not found." + return render(request, 'myapp/group_update.html', context) + except Exception as e: + context['error'] = f"Error retrieving group: {e}" + return render(request, 'myapp/group_update.html', context) + + if request.method == 'POST': + new_name = request.POST.get('name', '').strip() + if not new_name: + context['error'] = "New group name is required." + context['group'] = group + return render(request, 'myapp/group_update.html', context) + try: + # This assumes you've implemented an `update_group` function in your Rust library. + rust_crud_api.update_group(db_url, int(group_id), new_name) + return redirect('group_list') + except Exception as e: + context['error'] = f"Error updating group: {e}" + context['group'] = group + return render(request, 'myapp/group_update.html', context) + + context['group'] = group + return render(request, 'myapp/group_update.html', context) + +def group_delete_view(request, group_id): + """ + Delete a group. + """ + db_url = settings.DATABASE_URL + context = {} + if request.method == 'POST': + try: + # This assumes you have a `delete_group` function in your Rust library. + success = rust_crud_api.delete_group(db_url, int(group_id)) + if success: + return redirect('group_list') + else: + context['error'] = "Deletion failed." + except Exception as e: + context['error'] = f"Error deleting group: {e}" + else: + try: + group = rust_crud_api.get_group(db_url, int(group_id)) + context['group'] = group + except Exception as e: + context['error'] = f"Error retrieving group: {e}" + return render(request, 'myapp/group_delete.html', context) diff --git a/rust_crud_api/src/auth/jwt.rs b/rust_crud_api/src/auth/jwt.rs new file mode 100644 index 0000000000000000000000000000000000000000..e6ce0c7c51f724fd6d39ebdf1620ef541f326285 --- /dev/null +++ b/rust_crud_api/src/auth/jwt.rs @@ -0,0 +1,38 @@ +use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; +use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey}; +use serde::{Serialize, Deserialize}; +use pyo3::types::PyDict; + +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + sub: String, + exp: usize, +} + +#[pyfunction] +pub fn generate_jwt(user_email: String, secret: String, exp: usize) -> PyResult<String> { + let claims = Claims { sub: user_email, exp }; + encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_ref())) + .map_err(|e| PyRuntimeError::new_err(e.to_string())) +} + +#[pyfunction] +pub fn verify_jwt(token: String, secret: String) -> PyResult<PyObject> { + let token_data = decode::<Claims>( + &token, + &DecodingKey::from_secret(secret.as_ref()), + &Validation::default(), + ).map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + + let claims = token_data.claims; // Move claims inside the function scope + + Python::with_gil(|py| { + let dict = PyDict::new(py); + dict.set_item("sub", claims.sub)?; + dict.set_item("exp", claims.exp)?; + Ok(dict.to_object(py)) + }) +} + + diff --git a/rust_crud_api/src/auth/mod.rs b/rust_crud_api/src/auth/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..da8716c6ae6217ceaf7738e542cf8cc6f44217ef --- /dev/null +++ b/rust_crud_api/src/auth/mod.rs @@ -0,0 +1,6 @@ +pub mod jwt; +pub mod password; + +pub use jwt::{generate_jwt, verify_jwt}; +pub use password::{hash_password, verify_password}; + diff --git a/rust_crud_api/src/auth/password.rs b/rust_crud_api/src/auth/password.rs new file mode 100644 index 0000000000000000000000000000000000000000..01a94e0142fce0a74fe4f0679f4ffde384ff970c --- /dev/null +++ b/rust_crud_api/src/auth/password.rs @@ -0,0 +1,25 @@ +use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; +use argon2::{Argon2, PasswordHasher, PasswordVerifier}; +use argon2::password_hash::{SaltString, PasswordHash, rand_core::OsRng}; + +#[pyfunction] +pub fn hash_password(password: &str) -> PyResult<String> { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2.hash_password(password.as_bytes(), &salt) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .to_string(); + Ok(password_hash) +} + +#[pyfunction] +pub fn verify_password(hash: &str, password: &str) -> PyResult<bool> { + let argon2 = Argon2::default(); + let parsed_hash = PasswordHash::new(hash) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + argon2.verify_password(password.as_bytes(), &parsed_hash) + .map_err(|e| PyRuntimeError::new_err(e.to_string())) + .map(|_| true) +} + diff --git a/rust_crud_api/src/db/groups.rs b/rust_crud_api/src/db/groups.rs new file mode 100644 index 0000000000000000000000000000000000000000..deea586e00b30a52073d49d7632842abf0750ec1 --- /dev/null +++ b/rust_crud_api/src/db/groups.rs @@ -0,0 +1,98 @@ +use crate::models::Group; +use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; +use postgres::{Client, NoTls}; +use crate::models::User; + +fn pg_err(e: postgres::Error) -> PyErr { + PyRuntimeError::new_err(e.to_string()) +} + +/// Create a group +#[pyfunction] +pub fn create_group(db_url: &str, name: &str) -> PyResult<()> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + client.execute( + "INSERT INTO groups (name) VALUES ($1)", + &[&name] + ).map_err(pg_err)?; + Ok(()) +} + +/// Retrieve a group by ID. +#[pyfunction] +pub fn get_group(db_url: &str, group_id: i32) -> PyResult<Option<Group>> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let row_opt = client.query_opt( + "SELECT id, name FROM groups WHERE id = $1", + &[&group_id] + ).map_err(pg_err)?; + + if let Some(row) = row_opt { + let group = Group { + id: row.get(0), + name: row.get(1), + }; + Ok(Some(group)) + } else { + Ok(None) + } +} + +/// Retrieve all groups. +#[pyfunction] +pub fn get_all_groups(db_url: &str) -> PyResult<Vec<Group>> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let rows = client.query("SELECT id, name FROM groups", &[]) + .map_err(pg_err)?; + + let groups = rows.into_iter().map(|row| Group { + id: row.get(0), + name: row.get(1), + }).collect(); + + Ok(groups) +} + +/// Add a user to a group. +#[pyfunction] +pub fn add_user_to_group(db_url: &str, group_id: i32, user_id: i32) -> PyResult<()> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + client.execute( + "INSERT INTO group_members (group_id, user_id) VALUES ($1, $2)", + &[&group_id, &user_id] + ).map_err(pg_err)?; + Ok(()) +} + +/// Get all members of a group. +#[pyfunction] +pub fn get_group_members(db_url: &str, group_id: i32) -> PyResult<Vec<User>> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let rows = client.query( + "SELECT u.id, u.name, u.email + FROM users u + JOIN group_members gm ON u.id = gm.user_id + WHERE gm.group_id = $1", + &[&group_id] + ).map_err(pg_err)?; + + let users = rows.into_iter().map(|row| User { + id: row.get(0), + name: row.get(1), + email: row.get(2), + }).collect(); + + Ok(users) +} + +/// Group Delete Function +#[pyfunction] +pub fn delete_group(db_url: &str, group_id: i32) -> PyResult<bool> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let deleted = client.execute( + "DELETE FROM groups WHERE group_id = $1", + &[&group_id] + ).map_err(pg_err)?; + Ok(deleted > 0) +} diff --git a/rust_crud_api/src/db/init.rs b/rust_crud_api/src/db/init.rs new file mode 100644 index 0000000000000000000000000000000000000000..e0ecf0e7cf9af6dbde7aabc53288cbea51464efd --- /dev/null +++ b/rust_crud_api/src/db/init.rs @@ -0,0 +1,33 @@ +use postgres::{Client, NoTls}; +use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; + +fn pg_err(e: postgres::Error) -> PyErr { + PyRuntimeError::new_err(e.to_string()) +} + +#[pyfunction] +pub fn init_db(db_url: &str) -> PyResult<()> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + client.batch_execute( + " + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + email VARCHAR NOT NULL UNIQUE, + password_hash VARCHAR NOT NULL + ); + CREATE TABLE IF NOT EXISTS groups ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL + ); + CREATE TABLE IF NOT EXISTS group_members ( + group_id INTEGER REFERENCES groups(id), + user_id INTEGER REFERENCES users(id), + PRIMARY KEY (group_id, user_id) + ); + " + ).map_err(pg_err)?; + Ok(()) +} + diff --git a/rust_crud_api/src/db/mod.rs b/rust_crud_api/src/db/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..3ed49cab3f0f523156e2842f7af76625e173b216 --- /dev/null +++ b/rust_crud_api/src/db/mod.rs @@ -0,0 +1,8 @@ +pub mod users; +pub mod groups; +pub mod init; + +pub use init::init_db; +pub use users::{create_user, get_user, get_all_users, update_user, delete_user}; +pub use groups::{create_group, get_group, get_all_groups, add_user_to_group, get_group_members, delete_group}; + diff --git a/rust_crud_api/src/db/users.rs b/rust_crud_api/src/db/users.rs new file mode 100644 index 0000000000000000000000000000000000000000..3549b008f102297a6cafbb4192dbbe59b2823b31 --- /dev/null +++ b/rust_crud_api/src/db/users.rs @@ -0,0 +1,105 @@ +use crate::models::User; +use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; +use postgres::{Client, NoTls}; +use crate::auth::hash_password; +use crate::auth::verify_password; + +fn pg_err(e: postgres::Error) -> PyErr { + PyRuntimeError::new_err(e.to_string()) +} + + +/// Create a new user by inserting into the database. +#[pyfunction] +pub fn create_user(db_url: &str, name: &str, email: &str, password: &str) -> PyResult<()> { + let password_hash = hash_password(password)?; + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + client.execute( + "INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3)", + &[&name, &email, &password_hash] + ).map_err(pg_err)?; + Ok(()) +} + +/// Retrieve a user by ID. Returns None if the user is not found. +#[pyfunction] +pub fn get_user(db_url: &str, user_id: i32) -> PyResult<Option<User>> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let row_opt = client.query_opt( + "SELECT id, name, email FROM users WHERE id = $1", + &[&user_id] + ).map_err(pg_err)?; + + if let Some(row) = row_opt { + let user = User { + id: row.get(0), + name: row.get(1), + email: row.get(2), + }; + Ok(Some(user)) + } else { + Ok(None) + } +} + +/// Retrieve all users from the database. +#[pyfunction] +pub fn get_all_users(db_url: &str) -> PyResult<Vec<User>> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let rows = client.query("SELECT id, name, email FROM users", &[]) + .map_err(pg_err)?; + + let mut users = Vec::new(); + for row in rows { + users.push(User { + id: row.get(0), + name: row.get(1), + email: row.get(2), + }); + } + Ok(users) +} + +/// Update an existing user with a new name and email. +#[pyfunction] +pub fn update_user(db_url: &str, user_id: i32, new_name: &str, new_email: &str) -> PyResult<()> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let updated = client.execute( + "UPDATE users SET name = $1, email = $2 WHERE id = $3", + &[&new_name, &new_email, &user_id] + ).map_err(pg_err)?; + if updated == 0 { + Err(PyRuntimeError::new_err("User not found")) + } else { + Ok(()) + } +} + +/// Delete a user by ID. Returns true if a user was deleted. +#[pyfunction] +pub fn delete_user(db_url: &str, user_id: i32) -> PyResult<bool> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let deleted = client.execute( + "DELETE FROM users WHERE id = $1", + &[&user_id] + ).map_err(pg_err)?; + Ok(deleted > 0) +} + +///Verify credentials +///Retrieve a stored password hash for the email and compare. +#[pyfunction] +pub fn verify_user(db_url: &str, email: &str, password: & str) -> PyResult<bool> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let row_opt = client.query_opt( + "SELECT password_hash FROM users WHERE email = $1", + &[&email] + ).map_err(pg_err)?; + if let Some(row) = row_opt { + let stored_hash: String = row.get(0); + verify_password(&stored_hash, password) + } else { + Ok(false) + } +} diff --git a/rust_crud_api/src/lib.rs b/rust_crud_api/src/lib.rs index bc4f7b4dae2b557b1e5154f14a302dd46554fdc0..b8885a9739863c5329b25e66dd5c37371b13ef8f 100644 --- a/rust_crud_api/src/lib.rs +++ b/rust_crud_api/src/lib.rs @@ -1,329 +1,35 @@ -/// lib.rs +mod models; +mod auth; +mod db; use pyo3::prelude::*; -use pyo3::exceptions::PyRuntimeError; -use postgres::{Client, NoTls}; -use pyo3::types::PyDict; -use serde::{Serialize, Deserialize}; /// Import derive macros from serde_derive -use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey}; - -/// Define our User model, which will be exposed to Python. -#[pyclass] -#[derive(Serialize, Deserialize, Debug)] -struct User { - #[pyo3(get, set)] - id: Option<i32>, - #[pyo3(get, set)] - name: String, - #[pyo3(get, set)] - email: String, -} -/// Group Model -#[pyclass] -#[derive(Serialize, Deserialize, Debug)] -struct Group { - #[pyo3(get, set)] - id: Option<i32>, - #[pyo3(get, set)] - name: String, -} - -//JWT Claims for Token Payload - -#[derive(Debug,Serialize,Deserialize)] -struct Claims { - sub: String, // Subject - exp: usize, // Expiration time -} - -/// Generate a JWT token for a given user. -/// Returns the token as a String. -#[pyfunction] -fn generate_jwt(user_email: String, secret: String, exp: usize) -> PyResult<String> { - let claims = Claims { sub: user_email, exp }; - encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_ref())) - .map_err(|e| PyRuntimeError::new_err(e.to_string())) -} - -/// Verify a JWT token and return its claims as a Python dictionary. -#[pyfunction] -fn verify_jwt(token: String, secret: String) -> PyResult<PyObject> { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let token_data = decode::<Claims>( - &token, - &DecodingKey::from_secret(secret.as_ref()), - &Validation::default() - ).map_err(|e| PyRuntimeError::new_err(e.to_string()))?; - - // Extract the claims. - let claims = token_data.claims; - // Create a Python dictionary and insert the claim fields. - let dict = PyDict::new(py); - dict.set_item("sub", claims.sub)?; - dict.set_item("exp", claims.exp)?; - Ok(dict.to_object(py)) -} - -// Pasword Hashing with Argon2 - -#[pyfunction] -fn hash_password(password: &str) -> PyResult<String> { - use argon2::{Argon2, PasswordHasher}; - use argon2::password_hash::{SaltString, rand_core::OsRng}; - - // Generate a random salt using OsRng. - let salt = SaltString::generate(&mut OsRng); - // Create a default Argon2 instance. - let argon2 = Argon2::default(); - // Hash the password using the salt. - let password_hash = argon2.hash_password(password.as_bytes(), &salt) - .map_err(|e| PyRuntimeError::new_err(e.to_string()))? - .to_string(); - Ok(password_hash) -} - -#[pyfunction] -fn verify_password(hash: &str, password: &str) -> PyResult<bool> { - use argon2::{Argon2, PasswordVerifier}; - use argon2::password_hash::PasswordHash; - - let argon2 = Argon2::default(); - let parsed_hash = PasswordHash::new(hash) - .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; - argon2.verify_password(password.as_bytes(), &parsed_hash) - .map_err(|e| PyRuntimeError::new_err(e.to_string())) - .map(|_| true) -} - -/// A helper function to convert postgres::Error into a Python RuntimeError. -fn pg_err(e: postgres::Error) -> PyErr { - PyRuntimeError::new_err(e.to_string()) -} - -/// Initialize the database by creating the users table if it does not exist. -#[pyfunction] -fn init_db(db_url: &str) -> PyResult<()> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - client.batch_execute( - " - CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - email VARCHAR NOT NULL UNIQUE, - password_hash VARCHAR NOT NULL - ); - CREATE TABLE IF NOT EXISTS groups ( - id SERIAL PRIMARY KEY, - name VARCHAR NOT NULL - ); - CREATE TABLE IF NOT EXISTS group_members ( - group_id INTEGER REFERENCES groups(id), - user_id INTEGER REFERENCES users(id), - PRIMARY KEY (group_id, user_id) - ); - " - ).map_err(pg_err)?; - Ok(()) -} - -/// Create a new user by inserting into the database. -#[pyfunction] -fn create_user(db_url: &str, name: &str, email: &str, password: &str) -> PyResult<()> { - let password_hash = hash_password(password)?; - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - client.execute( - "INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3)", - &[&name, &email, &password_hash] - ).map_err(pg_err)?; - Ok(()) -} - -/// Retrieve a user by ID. Returns None if the user is not found. -#[pyfunction] -fn get_user(db_url: &str, user_id: i32) -> PyResult<Option<User>> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let row_opt = client.query_opt( - "SELECT id, name, email FROM users WHERE id = $1", - &[&user_id] - ).map_err(pg_err)?; - - if let Some(row) = row_opt { - let user = User { - id: row.get(0), - name: row.get(1), - email: row.get(2), - }; - Ok(Some(user)) - } else { - Ok(None) - } -} - -/// Retrieve all users from the database. -#[pyfunction] -fn get_all_users(db_url: &str) -> PyResult<Vec<User>> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let rows = client.query("SELECT id, name, email FROM users", &[]) - .map_err(pg_err)?; - - let mut users = Vec::new(); - for row in rows { - users.push(User { - id: row.get(0), - name: row.get(1), - email: row.get(2), - }); - } - Ok(users) -} - -/// Update an existing user with a new name and email. -#[pyfunction] -fn update_user(db_url: &str, user_id: i32, new_name: &str, new_email: &str) -> PyResult<()> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let updated = client.execute( - "UPDATE users SET name = $1, email = $2 WHERE id = $3", - &[&new_name, &new_email, &user_id] - ).map_err(pg_err)?; - if updated == 0 { - Err(PyRuntimeError::new_err("User not found")) - } else { - Ok(()) - } -} - -/// Delete a user by ID. Returns true if a user was deleted. -#[pyfunction] -fn delete_user(db_url: &str, user_id: i32) -> PyResult<bool> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let deleted = client.execute( - "DELETE FROM users WHERE id = $1", - &[&user_id] - ).map_err(pg_err)?; - Ok(deleted > 0) -} - -///Verify credentials -///Retrieve a stored password hash for the email and compare. -#[pyfunction] -fn verify_user(db_url: &str, email: &str, password: & str) -> PyResult<bool> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let row_opt = client.query_opt( - "SELECT password_hash FROM users WHERE email = $1", - &[&email] - ).map_err(pg_err)?; - if let Some(row) = row_opt { - let stored_hash: String = row.get(0); - verify_password(&stored_hash, password) - } else { - Ok(false) - } -} - - -/// Create a group -#[pyfunction] -fn create_group(db_url: &str, name: &str) -> PyResult<()> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - client.execute( - "INSERT INTO groups (name) VALUES ($1)", - &[&name] - ).map_err(pg_err)?; - Ok(()) -} - -/// Retrieve a group by ID. -#[pyfunction] -fn get_group(db_url: &str, group_id: i32) -> PyResult<Option<Group>> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let row_opt = client.query_opt( - "SELECT id, name FROM groups WHERE id = $1", - &[&group_id] - ).map_err(pg_err)?; - - if let Some(row) = row_opt { - let group = Group { - id: row.get(0), - name: row.get(1), - }; - Ok(Some(group)) - } else { - Ok(None) - } -} - -/// Retrieve all groups. -#[pyfunction] -fn get_all_groups(db_url: &str) -> PyResult<Vec<Group>> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let rows = client.query("SELECT id, name FROM groups", &[]) - .map_err(pg_err)?; - - let groups = rows.into_iter().map(|row| Group { - id: row.get(0), - name: row.get(1), - }).collect(); - - Ok(groups) -} - -/// Add a user to a group. -#[pyfunction] -fn add_user_to_group(db_url: &str, group_id: i32, user_id: i32) -> PyResult<()> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - client.execute( - "INSERT INTO group_members (group_id, user_id) VALUES ($1, $2)", - &[&group_id, &user_id] - ).map_err(pg_err)?; - Ok(()) -} - -/// Get all members of a group. -#[pyfunction] -fn get_group_members(db_url: &str, group_id: i32) -> PyResult<Vec<User>> { - let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let rows = client.query( - "SELECT u.id, u.name, u.email - FROM users u - JOIN group_members gm ON u.id = gm.user_id - WHERE gm.group_id = $1", - &[&group_id] - ).map_err(pg_err)?; - - let users = rows.into_iter().map(|row| User { - id: row.get(0), - name: row.get(1), - email: row.get(2), - }).collect(); - - Ok(users) -} - - -/// This is the Python module initializer. The name "rust_crud_api" here should match the name used in your Cargo.toml. #[pymodule] fn rust_crud_api(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::<User>()?; - m.add_class::<Group>()?; - m.add_function(wrap_pyfunction!(init_db, m)?)?; - m.add_function(wrap_pyfunction!(create_user, m)?)?; - m.add_function(wrap_pyfunction!(get_user, m)?)?; - m.add_function(wrap_pyfunction!(get_all_users, m)?)?; - m.add_function(wrap_pyfunction!(update_user, m)?)?; - m.add_function(wrap_pyfunction!(delete_user, m)?)?; - m.add_function(wrap_pyfunction!(create_group, m)?)?; - m.add_function(wrap_pyfunction!(get_group, m)?)?; - m.add_function(wrap_pyfunction!(get_all_groups, m)?)?; - m.add_function(wrap_pyfunction!(add_user_to_group, m)?)?; - m.add_function(wrap_pyfunction!(get_group_members, m)?)?; - m.add_function(wrap_pyfunction!(generate_jwt, m)?)?; - m.add_function(wrap_pyfunction!(verify_jwt, m)?)?; - m.add_function(wrap_pyfunction!(hash_password, m)?)?; - m.add_function(wrap_pyfunction!(verify_password, m)?)?; - m.add_function(wrap_pyfunction!(verify_user, m)?)?; + m.add_class::<models::User>()?; + m.add_class::<models::Group>()?; + + // Authentication functions + m.add_function(wrap_pyfunction!(auth::generate_jwt, m)?)?; + m.add_function(wrap_pyfunction!(auth::verify_jwt, m)?)?; + m.add_function(wrap_pyfunction!(auth::hash_password, m)?)?; + m.add_function(wrap_pyfunction!(auth::verify_password, m)?)?; + + // Database functions + m.add_function(wrap_pyfunction!(db::init::init_db, m)?)?; + m.add_function(wrap_pyfunction!(db::users::create_user, m)?)?; + m.add_function(wrap_pyfunction!(db::users::get_user, m)?)?; + m.add_function(wrap_pyfunction!(db::users::get_all_users, m)?)?; + m.add_function(wrap_pyfunction!(db::users::update_user, m)?)?; + m.add_function(wrap_pyfunction!(db::users::delete_user, m)?)?; + + m.add_function(wrap_pyfunction!(db::groups::create_group, m)?)?; + m.add_function(wrap_pyfunction!(db::groups::get_group, m)?)?; + m.add_function(wrap_pyfunction!(db::groups::get_all_groups, m)?)?; + m.add_function(wrap_pyfunction!(db::groups::add_user_to_group, m)?)?; + m.add_function(wrap_pyfunction!(db::groups::get_group_members, m)?)?; + m.add_function(wrap_pyfunction!(db::groups::delete_group, m)?)?; + Ok(()) } diff --git a/rust_crud_api/src/models/group.rs b/rust_crud_api/src/models/group.rs new file mode 100644 index 0000000000000000000000000000000000000000..ab2ebe36cf78d770dded31f21216dee76002080c --- /dev/null +++ b/rust_crud_api/src/models/group.rs @@ -0,0 +1,12 @@ +use pyo3::prelude::*; +use serde::{Serialize, Deserialize}; + +#[pyclass] +#[derive(Serialize, Deserialize, Debug)] +pub struct Group { + #[pyo3(get, set)] + pub id: Option<i32>, + #[pyo3(get, set)] + pub name: String, +} + diff --git a/rust_crud_api/src/models/mod.rs b/rust_crud_api/src/models/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..e2723a9a2c3acd1f557621efec1008d00567cd7e --- /dev/null +++ b/rust_crud_api/src/models/mod.rs @@ -0,0 +1,6 @@ +pub mod user; +pub mod group; + +pub use user::User; +pub use group::Group; + diff --git a/rust_crud_api/src/models/user.rs b/rust_crud_api/src/models/user.rs new file mode 100644 index 0000000000000000000000000000000000000000..042512cd2401ed57993a509ecbd3f67f3b052ae6 --- /dev/null +++ b/rust_crud_api/src/models/user.rs @@ -0,0 +1,14 @@ +use pyo3::prelude::*; +use serde::{Serialize, Deserialize}; + +#[pyclass] +#[derive(Serialize, Deserialize, Debug)] +pub struct User { + #[pyo3(get, set)] + pub id: Option<i32>, + #[pyo3(get, set)] + pub name: String, + #[pyo3(get, set)] + pub email: String, +} +