(feat): We've got a working admin panel
This commit is contained in:
		
							parent
							
								
									a8d2f7b798
								
							
						
					
					
						commit
						0628d45527
					
				
					 6 changed files with 205 additions and 78 deletions
				
			
		
							
								
								
									
										80
									
								
								app.py
									
										
									
									
									
								
							
							
						
						
									
										80
									
								
								app.py
									
										
									
									
									
								
							|  | @ -1,41 +1,42 @@ | |||
| import os | ||||
| import sqlite3 | ||||
| import smtplib | ||||
| from email.mime.text import MIMEText | ||||
| from flask import Flask, render_template, request, redirect, url_for, flash | ||||
| from flask import Flask, render_template, request, redirect, url_for, flash, session | ||||
| from dotenv import load_dotenv | ||||
| from werkzeug.security import check_password_hash | ||||
| from database import init_db, get_all_emails, get_admin, create_default_admin | ||||
| 
 | ||||
| load_dotenv() | ||||
| app = Flask(__name__) | ||||
| 
 | ||||
| # Use a secret key from .env; ensure your .env sets SECRET_KEY | ||||
| app.secret_key = os.getenv('SECRET_KEY') | ||||
| 
 | ||||
| DATABASE_URL = os.getenv('DATABASE_URL') | ||||
| # SMTP settings (for sending update emails) | ||||
| SMTP_SERVER = os.getenv('SMTP_SERVER') | ||||
| SMTP_PORT = int(os.getenv("SMTP_PORT", 465)) | ||||
| SMTP_USER = os.getenv('SMTP_USER') | ||||
| SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') | ||||
| 
 | ||||
| def get_all_emails(): | ||||
|     """Retrieve all subscriber emails from the database""" | ||||
|     try: | ||||
|         conn = sqlite3.connect(DATABASE_URL) | ||||
|         cursor = conn.cursor() | ||||
|         cursor.execute('SELECT email FROM subscribers') | ||||
|         results = cursor.fetchall() | ||||
|         conn.close() | ||||
|         return [row[0] for row in results] | ||||
|     except Exception as e: | ||||
|         print(f"Error: {e}") | ||||
|         return [] | ||||
| # Initialize the database and create default admin user if necessary. | ||||
| init_db() | ||||
| create_default_admin() | ||||
| 
 | ||||
| def send_update_email(subject, body): | ||||
|     """Send an update email""" | ||||
| def login_required(f): | ||||
|     from functools import wraps | ||||
|     @wraps(f) | ||||
|     def decorated_function(*args, **kwargs): | ||||
|         if "username" not in session: | ||||
|             return redirect(url_for('login')) | ||||
|         return f(*args, **kwargs) | ||||
|     return decorated_function | ||||
| 
 | ||||
| def process_send_update_email(subject, body): | ||||
|     """Helper function to send an update email to all subscribers.""" | ||||
|     subscribers = get_all_emails() | ||||
|     if not subscribers: | ||||
|         return "No subscribers found" | ||||
|         return "No subscribers found." | ||||
|     try: | ||||
|         server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) | ||||
|         server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) | ||||
|         server.set_debuglevel(True) | ||||
|         server.login(SMTP_USER, SMTP_PASSWORD) | ||||
|         for email in subscribers: | ||||
|  | @ -44,7 +45,7 @@ def send_update_email(subject, body): | |||
|             msg['From'] = SMTP_USER | ||||
|             msg['To'] = email | ||||
|             server.sendmail(SMTP_USER, email, msg.as_string()) | ||||
|             print(f"Updated email for {email} has been sent.") | ||||
|             print(f"Update email sent to: {email}") | ||||
|         server.quit() | ||||
|         return "Email has been sent." | ||||
|     except Exception as e: | ||||
|  | @ -52,21 +53,46 @@ def send_update_email(subject, body): | |||
|         return f"Failed to send email: {e}" | ||||
| 
 | ||||
| @app.route('/') | ||||
| @login_required | ||||
| def index(): | ||||
|     """Displays all subscriber emails""" | ||||
|     emails = get_all_emails() | ||||
|     return render_template("admin_index.html", emails=emails) | ||||
| 
 | ||||
| @app.route('/send_update_email', methods=['GET', 'POST']) | ||||
| def send_update_email(): | ||||
|     """Display a form to send an update email""" | ||||
| @app.route('/send_update', methods=['GET', 'POST']) | ||||
| @login_required | ||||
| def send_update(): | ||||
|     """Display a form to send an update email; process submission on POST.""" | ||||
|     if request.method == 'POST': | ||||
|         subject = request.form['subject'] | ||||
|         body = request.form['body'] | ||||
|         result_message = send_update_email(subject, body) | ||||
|         # Call the helper function using its new name. | ||||
|         result_message = process_send_update_email(subject, body) | ||||
|         flash(result_message) | ||||
|         return redirect(url_for("send_update_email")) | ||||
|         return redirect(url_for("send_update")) | ||||
|     return render_template("send_update.html") | ||||
| 
 | ||||
| @app.route('/login', methods=['GET', 'POST']) | ||||
| def login(): | ||||
|     if request.method == 'POST': | ||||
|         username = request.form.get('username') | ||||
|         password = request.form.get('password') | ||||
|         admin = get_admin(username) | ||||
|         # Expect get_admin() to return a tuple like (username, password_hash) | ||||
|         if admin and check_password_hash(admin[1], password): | ||||
|             session['username'] = username | ||||
|             flash("Logged in successfully", "success") | ||||
|             return redirect(url_for("index")) | ||||
|         else: | ||||
|             flash("Invalid username or password", "danger") | ||||
|             return redirect(url_for("login")) | ||||
|     return render_template("login.html") | ||||
| 
 | ||||
| @app.route('/logout') | ||||
| def logout(): | ||||
|     session.pop('username', None) | ||||
|     flash("Logged out successfully", "success") | ||||
|     return redirect(url_for("login")) | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     app.run(port=5001, debug=True) | ||||
|     app.run(port=5000, debug=True) | ||||
|  |  | |||
							
								
								
									
										148
									
								
								database.py
									
										
									
									
									
								
							
							
						
						
									
										148
									
								
								database.py
									
										
									
									
									
								
							|  | @ -1,65 +1,123 @@ | |||
| import sqlite3 | ||||
| import os | ||||
| import psycopg2 | ||||
| from psycopg2 import IntegrityError | ||||
| from dotenv import load_dotenv | ||||
| from werkzeug.security import generate_password_hash, check_password_hash | ||||
| 
 | ||||
| from werkzeug.security import generate_password_hash | ||||
| load_dotenv() | ||||
| DATABASE_URL = os.getenv("DATABASE_URL") | ||||
| 
 | ||||
| def get_connection(): | ||||
|     """Return a new connection to the PostgreSQL database.""" | ||||
|     return psycopg2.connect( | ||||
|         host=os.getenv("PG_HOST"), | ||||
|         port=os.getenv("PG_PORT"), | ||||
|         dbname=os.getenv("PG_DATABASE"), | ||||
|         user=os.getenv("PG_USER"), | ||||
|         password=os.getenv("PG_PASSWORD"), | ||||
|         connect_timeout=10 | ||||
|     ) | ||||
| 
 | ||||
| def init_db(): | ||||
|     with sqlite3.connect(DATABASE_URL, timeout=10) as conn: | ||||
|         cursor = conn.cursor() | ||||
|         cursor.execute(""" | ||||
|     """Initialize the database tables.""" | ||||
|     conn = get_connection() | ||||
|     cursor = conn.cursor() | ||||
|     # Create subscribers table (if not exists) | ||||
|     cursor.execute(""" | ||||
|         CREATE TABLE IF NOT EXISTS subscribers ( | ||||
|             id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
|             email TEXT UNIQUE NOT NULL) | ||||
|             """) | ||||
| 
 | ||||
|         cursor.execute(""" | ||||
|             CREATE TABLE IF NOT EXISTS admin_users ( | ||||
|                 id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
|                 username TEXT UNIQUE NOT NULL, | ||||
|                 password TEXT NOT NULL | ||||
|             ) | ||||
|         """) | ||||
|         conn.commit() | ||||
|             id SERIAL PRIMARY KEY, | ||||
|             email TEXT UNIQUE NOT NULL | ||||
|         ) | ||||
|     """) | ||||
|     # Create admin_users table (if not exists) | ||||
|     cursor.execute(""" | ||||
|         CREATE TABLE IF NOT EXISTS admin_users ( | ||||
|             id SERIAL PRIMARY KEY, | ||||
|             username TEXT UNIQUE NOT NULL, | ||||
|             password TEXT NOT NULL | ||||
|         ) | ||||
|     """) | ||||
|     conn.commit() | ||||
|     cursor.close() | ||||
|     conn.close() | ||||
| 
 | ||||
| def get_all_emails(): | ||||
|     """Return a list of all subscriber emails.""" | ||||
|     try: | ||||
|         with sqlite3.connect(DATABASE_URL, timeout=10) as conn: | ||||
|             cursor = conn.cursor() | ||||
|             cursor.execute("SELECT email FROM subscribers") | ||||
|             results = cursor.fetchall() | ||||
|         conn = get_connection() | ||||
|         cursor = conn.cursor() | ||||
|         cursor.execute("SELECT email FROM subscribers") | ||||
|         results = cursor.fetchall() | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|         return [row[0] for row in results] | ||||
|     except Exception as e: | ||||
|         print(f"Error: {e}") | ||||
|         print(f"Error retrieving emails: {e}") | ||||
|         return [] | ||||
| 
 | ||||
| def get_admin(username): | ||||
| def add_email(email): | ||||
|     """Insert an email into the subscribers table.""" | ||||
|     try: | ||||
|         with sqlite3.connect(DATABASE_URL, timeout=10) as conn: | ||||
|             cursor = conn.cursor() | ||||
|             cursor.execute("SELECT username FROM admin_users WHERE username=?", (username,)) | ||||
|             results = cursor.fetchone() | ||||
|         conn = get_connection() | ||||
|         cursor = conn.cursor() | ||||
|         cursor.execute("INSERT INTO subscribers (email) VALUES (%s)", (email,)) | ||||
|         conn.commit() | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|         return True | ||||
|     except IntegrityError: | ||||
|         return False | ||||
|     except Exception as e: | ||||
|         print(f"Error: {e}") | ||||
|         print(f"Error adding email: {e}") | ||||
|         return False | ||||
| 
 | ||||
| def remove_email(email): | ||||
|     """Remove an email from the subscribers table.""" | ||||
|     try: | ||||
|         conn = get_connection() | ||||
|         cursor = conn.cursor() | ||||
|         cursor.execute("DELETE FROM subscribers WHERE email = %s", (email,)) | ||||
|         conn.commit() | ||||
|         rowcount = cursor.rowcount | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|         return rowcount > 0 | ||||
|     except Exception as e: | ||||
|         print(f"Error removing email: {e}") | ||||
|         return False | ||||
| 
 | ||||
| def get_admin(username): | ||||
|     """Retrieve admin credentials for a given username. | ||||
|        Returns a tuple (username, password_hash) if found, otherwise None. | ||||
|     """ | ||||
|     try: | ||||
|         conn = get_connection() | ||||
|         cursor = conn.cursor() | ||||
|         cursor.execute("SELECT username, password FROM admin_users WHERE username = %s", (username,)) | ||||
|         result = cursor.fetchone() | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|         return result  # (username, password_hash) | ||||
|     except Exception as e: | ||||
|         print(f"Error retrieving admin: {e}") | ||||
|         return None | ||||
| 
 | ||||
| def create_default_admin(): | ||||
|     default_username = os.getenv("DEFAULT_ADMIN_USERNAME") | ||||
|     default_password = os.getenv("DEFAULT_ADMIN_PASSWORD") | ||||
|     hashed = generate_password_hash(default_password, method='sha256') | ||||
| 
 | ||||
|     """Create a default admin user if one doesn't already exist.""" | ||||
|     default_username = os.getenv("ADMIN_USERNAME", "admin") | ||||
|     default_password = os.getenv("ADMIN_PASSWORD", "changeme") | ||||
|     hashed = generate_password_hash(default_password, method="pbkdf2:sha256") | ||||
|     try: | ||||
|         with sqlite3.connect(DATABASE_URL, timeout=10) as conn: | ||||
|             cursor = conn.cursor() | ||||
|             cursor.execute("SELECT id FROM admin_users WHERE username = ?", (default_username,)) | ||||
|             if cursor.fetchone() is not None: | ||||
|                 cursor.execute("INSERT INTO admin_users (username, password) VALUES (?, ?)", | ||||
|                                (default_username, hashed)) | ||||
|                 conn.commit() | ||||
|                 print("Admin user created successfully") | ||||
|             else: | ||||
|                 print("Admin user creation failed") | ||||
|         conn = get_connection() | ||||
|         cursor = conn.cursor() | ||||
|         # Check if the admin already exists | ||||
|         cursor.execute("SELECT id FROM admin_users WHERE username = %s", (default_username,)) | ||||
|         if cursor.fetchone() is None: | ||||
|             cursor.execute("INSERT INTO admin_users (username, password) VALUES (%s, %s)", | ||||
|                            (default_username, hashed)) | ||||
|             conn.commit() | ||||
|             print("Default admin created successfully") | ||||
|         else: | ||||
|             print("Default admin already exists") | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|     except Exception as e: | ||||
|         print(f"Error: {e}") | ||||
|         print(f"Error creating default admin: {e}") | ||||
|  |  | |||
|  | @ -1,2 +1,4 @@ | |||
| flask | ||||
| python-dotenv | ||||
| python-dotenv | ||||
| Werkzeug | ||||
| psycopg2-binary | ||||
|  | @ -14,7 +14,7 @@ | |||
| </head> | ||||
| <body> | ||||
|     <h1>Subscribers</h1> | ||||
|   <p><a href="{{ url_for('send_update_email') }}">Send Update Email</a></p> | ||||
|   <p><a href="{{ url_for('send_update') }}">Send Update Email</a></p> | ||||
|   {% if emails %} | ||||
|     <table> | ||||
|       <tr> | ||||
|  |  | |||
							
								
								
									
										33
									
								
								templates/login.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								templates/login.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="UTF-8" /> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|   <title>Admin Login</title> | ||||
|   <style> | ||||
|     body { font-family: Arial, sans-serif; padding: 20px; } | ||||
|     form { max-width: 400px; margin: 0 auto; } | ||||
|     label { display: block; margin-top: 15px; } | ||||
|     input[type="text"], input[type="password"] { width: 100%; padding: 8px; } | ||||
|     button { margin-top: 15px; padding: 10px 20px; } | ||||
|     .flash { background-color: #f8d7da; color: #721c24; padding: 10px; margin-bottom: 10px; text-align: center; } | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|   <h1>Admin Login</h1> | ||||
|   {% with messages = get_flashed_messages(with_categories=true) %} | ||||
|     {% if messages %} | ||||
|       {% for category, message in messages %} | ||||
|         <div class="flash">{{ message }}</div> | ||||
|       {% endfor %} | ||||
|     {% endif %} | ||||
|   {% endwith %} | ||||
|   <form action="{{ url_for('login') }}" method="POST"> | ||||
|     <label for="username">Username:</label> | ||||
|     <input type="text" name="username" required /> | ||||
|     <label for="password">Password:</label> | ||||
|     <input type="password" name="password" required /> | ||||
|     <button type="submit">Login</button> | ||||
|   </form> | ||||
| </body> | ||||
| </html> | ||||
|  | @ -1,7 +1,7 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="UTF-8"> | ||||
|   <meta charset="UTF-8" /> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|   <title>Admin Center - Send Update</title> | ||||
|   <style> | ||||
|  | @ -10,7 +10,12 @@ | |||
|     label { display: block; margin-top: 15px; } | ||||
|     input[type="text"], textarea { width: 100%; padding: 8px; } | ||||
|     button { margin-top: 15px; padding: 10px 20px; } | ||||
|     .flash { background-color: #f8d7da; color: #721c24; padding: 10px; margin-bottom: 10px; } | ||||
|     .flash { | ||||
|       background-color: #f8d7da; | ||||
|       color: #721c24; | ||||
|       padding: 10px; | ||||
|       margin-bottom: 10px; | ||||
|     } | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|  | @ -23,7 +28,7 @@ | |||
|     {% endif %} | ||||
|   {% endwith %} | ||||
| 
 | ||||
|   <form action="{{ url_for('send_update_email') }}" method="POST"> | ||||
|   <form action="{{ url_for('send_update') }}" method="POST"> | ||||
|     <label for="subject">Subject:</label> | ||||
|     <input type="text" name="subject" required> | ||||
| 
 | ||||
|  | @ -32,6 +37,9 @@ | |||
| 
 | ||||
|     <button type="submit">Send Update</button> | ||||
|   </form> | ||||
|   <p><a href="{{ url_for('index') }}">Back to Subscribers List</a></p> | ||||
|   <p> | ||||
|     <a href="{{ url_for('index') }}">Back to Subscribers List</a> | | ||||
|     <a href="{{ url_for('logout') }}">Logout</a> | ||||
|   </p> | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Blake Ridgway
						Blake Ridgway