diff --git a/app.py b/app.py index 754661e..46412e7 100644 --- a/app.py +++ b/app.py @@ -23,12 +23,21 @@ class User(UserMixin, db.Model): username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(120), nullable=False) is_admin = db.Column(db.Boolean, default=False) + can_add_recipes = db.Column(db.Boolean, default=False) + recipes = db.relationship('Recipe', backref='author', lazy=True) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) class Recipe(db.Model): id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(200), nullable=False) + title = db.Column(db.String(100), nullable=False) ingredients = db.Column(db.Text, nullable=False) instructions = db.Column(db.Text, nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) @login_manager.user_loader def load_user(user_id): @@ -54,19 +63,25 @@ def recipe_detail(recipe_id): recipe = Recipe.query.get_or_404(recipe_id) return render_template('recipe_detail.html', recipe=recipe) -@app.route('/admin', methods=['GET']) +@app.route('/admin') @login_required def admin(): - if not current_user.is_admin: + if not current_user.is_admin and not current_user.can_add_recipes: flash('Access denied.') return redirect(url_for('index')) - recipes = Recipe.query.all() + + # Admin sees all recipes, others see only their own + if current_user.is_admin: + recipes = Recipe.query.all() + else: + recipes = Recipe.query.filter_by(user_id=current_user.id).all() + return render_template('admin.html', recipes=recipes) @app.route('/admin/recipe/add', methods=['GET', 'POST']) @login_required def add_recipe(): - if not current_user.is_admin: + if not current_user.is_admin and not current_user.can_add_recipes: flash('Access denied.') return redirect(url_for('index')) @@ -74,23 +89,29 @@ def add_recipe(): recipe = Recipe( title=request.form['title'], ingredients=request.form['ingredients'], - instructions=request.form['instructions'] + instructions=request.form['instructions'], + user_id=current_user.id ) db.session.add(recipe) db.session.commit() flash('Recipe added successfully!') return redirect(url_for('admin')) - - return render_template('recipe_form.html') + return render_template('add_recipe.html') @app.route('/admin/recipe/edit/', methods=['GET', 'POST']) @login_required def edit_recipe(recipe_id): - if not current_user.is_admin: + if not current_user.is_admin and not current_user.can_add_recipes: flash('Access denied.') return redirect(url_for('index')) recipe = Recipe.query.get_or_404(recipe_id) + + # Only allow editing if admin or if user created the recipe + if not current_user.is_admin and recipe.user_id != current_user.id: + flash('You can only edit your own recipes.') + return redirect(url_for('admin')) + if request.method == 'POST': recipe.title = request.form['title'] recipe.ingredients = request.form['ingredients'] @@ -98,22 +119,97 @@ def edit_recipe(recipe_id): db.session.commit() flash('Recipe updated successfully!') return redirect(url_for('admin')) - - return render_template('recipe_form.html', recipe=recipe) + return render_template('edit_recipe.html', recipe=recipe) @app.route('/admin/recipe/delete/') @login_required def delete_recipe(recipe_id): - if not current_user.is_admin: + if not current_user.is_admin and not current_user.can_add_recipes: flash('Access denied.') return redirect(url_for('index')) recipe = Recipe.query.get_or_404(recipe_id) + + # Only allow deletion if admin or if user created the recipe + if not current_user.is_admin and recipe.user_id != current_user.id: + flash('You can only delete your own recipes.') + return redirect(url_for('admin')) + db.session.delete(recipe) db.session.commit() flash('Recipe deleted successfully!') return redirect(url_for('admin')) +@app.route('/users') +@login_required +def list_users(): + if not current_user.is_admin: + flash('Access denied.') + return redirect(url_for('index')) + users = User.query.all() + return render_template('users.html', users=users) + +@app.route('/users/add', methods=['GET', 'POST']) +@login_required +def add_user(): + if not current_user.is_admin: + flash('Access denied.') + return redirect(url_for('index')) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + can_add_recipes = 'can_add_recipes' in request.form + + if User.query.filter_by(username=username).first(): + flash('Username already exists.') + return redirect(url_for('add_user')) + + user = User(username=username, can_add_recipes=can_add_recipes) + user.set_password(password) + db.session.add(user) + db.session.commit() + flash('User added successfully.') + return redirect(url_for('list_users')) + + return render_template('add_user.html') + +@app.route('/users//edit', methods=['GET', 'POST']) +@login_required +def edit_user(id): + if not current_user.is_admin: + flash('Access denied.') + return redirect(url_for('index')) + + user = User.query.get_or_404(id) + if request.method == 'POST': + user.username = request.form.get('username') + if request.form.get('password'): + user.set_password(request.form.get('password')) + user.can_add_recipes = 'can_add_recipes' in request.form + db.session.commit() + flash('User updated successfully.') + return redirect(url_for('list_users')) + + return render_template('edit_user.html', user=user) + +@app.route('/users//delete', methods=['POST']) +@login_required +def delete_user(id): + if not current_user.is_admin: + flash('Access denied.') + return redirect(url_for('index')) + + user = User.query.get_or_404(id) + if user.is_admin: + flash('Cannot delete admin user.') + return redirect(url_for('list_users')) + + db.session.delete(user) + db.session.commit() + flash('User deleted successfully.') + return redirect(url_for('list_users')) + # HTMX Endpoints @app.route('/recipes/search') def search_recipes(): @@ -127,7 +223,7 @@ def search_recipes(): @app.route('/recipe//delete', methods=['DELETE']) @login_required def delete_recipe_htmx(recipe_id): - if not current_user.is_admin: + if not current_user.is_admin and not current_user.can_add_recipes: return 'Unauthorized', 403 recipe = Recipe.query.get_or_404(recipe_id) @@ -143,9 +239,11 @@ def health_check(): def login(): if request.method == 'POST': user = User.query.filter_by(username=request.form['username']).first() - if user and check_password_hash(user.password_hash, request.form['password']): + if user and user.check_password(request.form['password']): login_user(user) - return redirect(url_for('admin')) + if user.is_admin or user.can_add_recipes: + return redirect(url_for('admin')) + return redirect(url_for('index')) flash('Invalid username or password') return render_template('login.html') diff --git a/init_db.py b/init_db.py index 521c28a..2641194 100644 --- a/init_db.py +++ b/init_db.py @@ -1,29 +1,52 @@ -from app import app, db, User +from app import app, db, User, Recipe from werkzeug.security import generate_password_hash -from dotenv import load_dotenv import os +from dotenv import load_dotenv -# Load environment variables load_dotenv() def init_db(): with app.app_context(): + # Create tables + db.drop_all() db.create_all() - - # Check if admin user already exists - admin_username = os.getenv('ADMIN_USERNAME', 'admin') - admin = User.query.filter_by(username=admin_username).first() - if not admin: - admin = User( - username=admin_username, - password_hash=generate_password_hash(os.getenv('ADMIN_PASSWORD', 'change-this-password')), - is_admin=True - ) - db.session.add(admin) - db.session.commit() - print("Admin user created successfully!") - else: - print("Admin user already exists.") + + # Create admin user + admin = User( + username=os.getenv('ADMIN_USERNAME', 'admin'), + is_admin=True, + can_add_recipes=True + ) + admin.set_password(os.getenv('ADMIN_PASSWORD', 'admin')) + db.session.add(admin) + + # Create a recipe manager user + manager = User( + username='recipe_manager', + can_add_recipes=True + ) + manager.set_password('manager123') + db.session.add(manager) + + # Add some sample recipes + recipes = [ + { + 'title': 'Chocolate Chip Cookies', + 'ingredients': '2 1/4 cups all-purpose flour\n1 cup butter\n3/4 cup sugar\n2 eggs\n2 cups chocolate chips', + 'instructions': '1. Preheat oven to 375°F\n2. Mix ingredients\n3. Drop spoonfuls onto baking sheet\n4. Bake for 10 minutes' + }, + { + 'title': 'Classic Pancakes', + 'ingredients': '1 1/2 cups all-purpose flour\n3 1/2 teaspoons baking powder\n1 teaspoon salt\n1 tablespoon sugar', + 'instructions': '1. Mix dry ingredients\n2. Add wet ingredients\n3. Cook on griddle' + } + ] + + for recipe_data in recipes: + recipe = Recipe(**recipe_data) + db.session.add(recipe) + + db.session.commit() if __name__ == '__main__': init_db() diff --git a/templates/add_user.html b/templates/add_user.html new file mode 100644 index 0000000..2451519 --- /dev/null +++ b/templates/add_user.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block content %} +
+

Add New User

+
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ Cancel + +
+
+
+ + +{% endblock %} diff --git a/templates/admin.html b/templates/admin.html index 19423c4..a36d701 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -1,30 +1,107 @@ {% extends "base.html" %} {% block content %} -
-

Recipe Management

- Add New Recipe +
+
+

{% if current_user.is_admin %}Recipe Management{% else %}My Recipes{% endif %}

+
+ {% if current_user.is_admin %} + Manage Users + {% endif %} + Add New Recipe + Logout +
+
+ +
+ + + + + + + + + + {% for recipe in recipes %} + + + + + + {% endfor %} + +
TitleCreated ByActions
{{ recipe.title }}{{ recipe.author.username if recipe.author else 'Unknown' }} + {% if current_user.is_admin or recipe.user_id == current_user.id %} + Edit + Delete + {% endif %} +
+
-
- - - - - - - - - {% for recipe in recipes %} - - - - - {% endfor %} - -
TitleActions
{{ recipe.title }} - Edit - Delete -
-
+ {% endblock %} diff --git a/templates/base.html b/templates/base.html index 867d90b..ce2b82a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -19,13 +19,13 @@
diff --git a/templates/edit_user.html b/templates/edit_user.html new file mode 100644 index 0000000..b8916b8 --- /dev/null +++ b/templates/edit_user.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block content %} +
+

Edit User: {{ user.username }}

+
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ Cancel + +
+
+
+ + +{% endblock %} diff --git a/templates/users.html b/templates/users.html new file mode 100644 index 0000000..9797d14 --- /dev/null +++ b/templates/users.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} + +{% block content %} +
+

User Management

+ + +
+ + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
UsernamePermissionsActions
{{ user.username }} + {% if user.is_admin %} + Admin + {% elif user.can_add_recipes %} + Recipe Manager + {% else %} + Viewer + {% endif %} + + {% if not user.is_admin %} + Edit +
+ +
+ {% endif %} +
+
+
+ + +{% endblock %}