diff --git a/app.py b/app.py new file mode 100644 index 0000000..3b8cc80 --- /dev/null +++ b/app.py @@ -0,0 +1,238 @@ +from flask import Flask, render_template, request, redirect, url_for, flash, abort, make_response, session +from flask_sqlalchemy import SQLAlchemy +import datetime +import json +import random +from argon2 import PasswordHasher +from argon2.exceptions import VerifyMismatchError + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///quotes.db' +app.config['SECRET_KEY'] = 'your_secret_key' # Use environment variable in production +db = SQLAlchemy(app) + +# Initialize Argon2 password hasher +ph = PasswordHasher() + +# Hardcoded admin credentials (hashed password using Argon2) +ADMIN_CREDENTIALS = { + 'username': 'admin', + # Replace this with the hashed password generated by Argon2 + 'password': '$argon2i$v=19$m=65536,t=4,p=1$cWZDc1pQaUJLTUJoaVI4cw$kn8XKz6AEZi8ebXfyyZuzommSypliVFrsGqzOyUEIHA' # Example hash +} + +# Define the Quote model +class Quote(db.Model): + id = db.Column(db.Integer, primary_key=True) + text = db.Column(db.Text, nullable=False) + votes = db.Column(db.Integer, default=0) + date = db.Column(db.DateTime, default=datetime.datetime.utcnow) + status = db.Column(db.Integer, default=0) # 0 = pending, 1 = approved, 2 = rejected + +# Home route to display quotes +# Home route to display quotes +@app.route('/') +def index(): + page = request.args.get('page', 1, type=int) + quotes = Quote.query.filter_by(status=1).order_by(Quote.date.desc()).paginate(page=page, per_page=5) + + # Get the count of approved and pending quotes + approved_count = Quote.query.filter_by(status=1).count() + pending_count = Quote.query.filter_by(status=0).count() + + return render_template('index.html', quotes=quotes, approved_count=approved_count, pending_count=pending_count) + +# Separate route for submitting quotes +@app.route('/submit', methods=['GET', 'POST']) +def submit(): + if request.method == 'POST': + quote_text = request.form.get('quote') + if not quote_text: + flash("Quote cannot be empty.", 'error') + return redirect(url_for('submit')) + + new_quote = Quote(text=quote_text) + try: + db.session.add(new_quote) + db.session.commit() + flash("Quote submitted successfully!", 'success') + except Exception as e: + db.session.rollback() + flash("Error submitting quote: {}".format(e), 'error') + + return redirect(url_for('index')) + + # Get the count of approved and pending quotes + approved_count = Quote.query.filter_by(status=1).count() + pending_count = Quote.query.filter_by(status=0).count() + + return render_template('submit.html', approved_count=approved_count, pending_count=pending_count) + + +# Route to handle voting (upvote/downvote) +@app.route('/vote//') +def vote(id, action): + quote = Quote.query.get_or_404(id) + + # Check if the user has already voted on this quote + vote_cookie = request.cookies.get('votes') + if vote_cookie: + vote_data = json.loads(vote_cookie) + else: + vote_data = {} + + # If the user has already voted on this quote, check if they're trying to undo the vote + if str(id) in vote_data: + previous_action = vote_data[str(id)] + + if previous_action == action: + # User is trying to undo their vote + if action == 'upvote': + quote.votes -= 1 + elif action == 'downvote': + quote.votes += 1 + del vote_data[str(id)] # Remove the vote from the cookie + flash("Your vote has been undone.", 'success') + else: + # User is switching their vote (upvote to downvote or vice versa) + if action == 'upvote': + quote.votes += 2 # Switch from downvote to upvote + elif action == 'downvote': + quote.votes -= 2 # Switch from upvote to downvote + vote_data[str(id)] = action # Update the action in the cookie + flash("Your vote has been changed.", 'success') + else: + # User has not voted on this quote before + if action == 'upvote': + quote.votes += 1 + elif action == 'downvote': + quote.votes -= 1 + vote_data[str(id)] = action # Record the vote in the cookie + flash("Thank you for voting!", 'success') + + # Save the updated vote data in the cookie + try: + db.session.commit() + page = request.args.get('page', 1) # Get the current page number from query params + resp = make_response(redirect(url_for('browse', page=page))) + resp.set_cookie('votes', json.dumps(vote_data), max_age=60*60*24*365) # 1 year expiration + return resp + except Exception as e: + db.session.rollback() + flash("Error voting on quote: {}".format(e), 'error') + return redirect(url_for('browse', page=page)) + + +# Route for displaying a random quote +@app.route('/random') +def random_quote(): + count = Quote.query.count() + if count == 0: + flash("No quotes available yet.", 'error') + return redirect(url_for('index')) + + random_id = random.randint(1, count) + random_quote = Quote.query.get(random_id) + + # If the random quote is not found (i.e., deleted), pick another random quote + while random_quote is None: + random_id = random.randint(1, count) + random_quote = Quote.query.get(random_id) + + return render_template('random.html', quote=random_quote) + +@app.route('/quote') +def quote(): + quote_id = request.args.get('id') + if not quote_id: + flash("Quote ID not provided.", 'error') + return redirect(url_for('browse')) + + quote = Quote.query.get_or_404(quote_id) + return render_template('quote.html', quote=quote) + +# Admin login route +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + # Check if the username is correct and verify the password using Argon2 + if username == ADMIN_CREDENTIALS['username']: + try: + ph.verify(ADMIN_CREDENTIALS['password'], password) # Verify password using Argon2 + session['admin'] = True + flash('Login successful!', 'success') + return redirect(url_for('modapp')) + except VerifyMismatchError: + flash('Invalid password. Please try again.', 'danger') + else: + flash('Invalid username. Please try again.', 'danger') + + return render_template('login.html') + +# Admin panel route (accessible only to logged-in admins) +@app.route('/modapp') +def modapp(): + if not session.get('admin'): # If admin not logged in, redirect to login + flash('You need to log in first.', 'danger') + return redirect(url_for('login')) + + pending_quotes = Quote.query.filter_by(status=0).all() + return render_template('modapp.html', pending_quotes=pending_quotes) + +# Route for browsing approved quotes +@app.route('/browse', methods=['GET']) +def browse(): + page = request.args.get('page', 1, type=int) + quotes = Quote.query.filter_by(status=1).order_by(Quote.date.desc()).paginate(page=page, per_page=10) + return render_template('browse.html', quotes=quotes) + +# Approve a quote (admin only) +@app.route('/approve/') +def approve(id): + if not session.get('admin'): + return redirect(url_for('login')) + + quote = Quote.query.get_or_404(id) + quote.status = 1 # 1 = approved + db.session.commit() + return redirect(url_for('modapp')) + +# Reject a quote (admin only) +@app.route('/reject/') +def reject(id): + if not session.get('admin'): + return redirect(url_for('login')) + + quote = Quote.query.get_or_404(id) + quote.status = 2 # 2 = rejected + db.session.commit() + return redirect(url_for('modapp')) + +# Delete a quote (admin only) +@app.route('/delete/') +def delete(id): + if not session.get('admin'): + return redirect(url_for('login')) + + quote = Quote.query.get_or_404(id) + db.session.delete(quote) + db.session.commit() + return redirect(url_for('modapp')) + +# Admin logout route +@app.route('/logout') +def logout(): + session.pop('admin', None) + flash('Logged out successfully.', 'success') + return redirect(url_for('login')) + +# Automatically create the database tables using app context +with app.app_context(): + db.create_all() + +# Run the Flask app +if __name__ == "__main__": + app.run(debug=True)