From a569ac5f14a218949348d81d142820d2ef0757a5 Mon Sep 17 00:00:00 2001 From: Bryan Cerrati Date: Fri, 7 Mar 2025 11:00:27 -0500 Subject: [PATCH] Passwords Working --- app/__init__.py | 17 ++++- app/models.py | 35 +++++++--- app/routes/clients.py | 44 ++++++++----- app/routes/tools.py | 41 ++++++++++-- app/templates/client_detail.html | 90 ++++++++++++++++++-------- app/templates/password_categories.html | 44 +++++++++++++ app/templates/ssl.html | 4 +- app/templates/tools.html | 12 ++-- 8 files changed, 224 insertions(+), 63 deletions(-) create mode 100644 app/templates/password_categories.html diff --git a/app/__init__.py b/app/__init__.py index d935939..8519896 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,9 @@ import os from flask import Flask from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import event +from sqlalchemy.engine import Engine + db = SQLAlchemy() @@ -17,10 +20,22 @@ def create_app(): # Initialize SQLAlchemy with the app db.init_app(app) + # Enable SQLite foreign key support. with app.app_context(): - # Import models so that they are registered with SQLAlchemy before creating tables from app import models db.create_all() + # Ensure the default password category "General" exists + from app.models import PasswordCategory + if not PasswordCategory.query.filter_by(name="General").first(): + default_category = PasswordCategory(name="General") + db.session.add(default_category) + db.session.commit() + + @event.listens_for(Engine, "connect") + def set_sqlite_pragma(dbapi_connection, connection_record): + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() # Register blueprints from app.routes.domain import domain_bp diff --git a/app/models.py b/app/models.py index fa2b5f8..ac38d09 100644 --- a/app/models.py +++ b/app/models.py @@ -1,25 +1,25 @@ from app import db -# Association tables for many-to-many relationships. +# Association tables (if not already defined) client_domains = db.Table('client_domains', - db.Column('client_id', db.Integer, db.ForeignKey('client.id')), + db.Column('client_id', db.Integer, db.ForeignKey('client.id', ondelete="CASCADE")), db.Column('domain_id', db.Integer, db.ForeignKey('domain.id')) ) client_ssl = db.Table('client_ssl', - db.Column('client_id', db.Integer, db.ForeignKey('client.id')), + db.Column('client_id', db.Integer, db.ForeignKey('client.id', ondelete="CASCADE")), db.Column('domain_id', db.Integer, db.ForeignKey('domain.id')) ) + class Domain(db.Model): __tablename__ = 'domain' __table_args__ = {'extend_existing': True} - id = db.Column(db.Integer, primary_key=True) domain = db.Column(db.String(255), unique=True, nullable=False) expiration_date = db.Column(db.String(255)) # WHOIS expiration date ssl_expiration_date = db.Column(db.String(255)) # SSL certificate expiration date - ssl_only = db.Column(db.Boolean, default=False) # True if added via the SSL-only form + ssl_only = db.Column(db.Boolean, default=False) # True if added via SSL-only form def __repr__(self): return f"" @@ -31,15 +31,28 @@ class Client(db.Model): phone = db.Column(db.String(255)) address = db.Column(db.String(255)) - # Relationship for storing passwords - passwords = db.relationship('ClientPassword', backref='client', lazy=True) - # Many-to-many relationships for domains and SSL domains - domains = db.relationship('Domain', secondary=client_domains, backref='clients') - ssl_domains = db.relationship('Domain', secondary=client_ssl, backref='ssl_clients') + # Delete all passwords when the client is deleted. + passwords = db.relationship('ClientPassword', backref='client', lazy=True, + cascade="all, delete-orphan") + + # Many-to-many: Only the associations will be removed (due to ondelete cascade), not the Domain records. + domains = db.relationship('Domain', secondary=client_domains, + backref=db.backref('clients', lazy='dynamic')) + ssl_domains = db.relationship('Domain', secondary=client_ssl, + backref=db.backref('ssl_clients', lazy='dynamic')) def __repr__(self): return f"" + +class PasswordCategory(db.Model): + __tablename__ = 'password_category' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), unique=True, nullable=False) + + def __repr__(self): + return f"" + class ClientPassword(db.Model): __tablename__ = 'client_password' id = db.Column(db.Integer, primary_key=True) @@ -47,6 +60,8 @@ class ClientPassword(db.Model): username = db.Column(db.String(255), nullable=False) password = db.Column(db.String(255), nullable=False) client_id = db.Column(db.Integer, db.ForeignKey('client.id'), nullable=False) + category_id = db.Column(db.Integer, db.ForeignKey('password_category.id')) + category = db.relationship('PasswordCategory') def __repr__(self): return f"" diff --git a/app/routes/clients.py b/app/routes/clients.py index 7b61359..56d1b22 100644 --- a/app/routes/clients.py +++ b/app/routes/clients.py @@ -1,16 +1,14 @@ -from flask import Blueprint, render_template, request, redirect, url_for -from app.models import Client, ClientPassword, Domain +from flask import Blueprint, render_template, request, redirect, url_for, flash +from app.models import Client, ClientPassword, Domain, PasswordCategory from app import db clients_bp = Blueprint('clients', __name__) -# List all clients @clients_bp.route('/clients') def clients(): clients_list = Client.query.all() return render_template('clients.html', clients=clients_list) -# Add multiple clients at once @clients_bp.route('/clients/add', methods=['GET', 'POST']) def add_client(): if request.method == 'POST': @@ -23,19 +21,25 @@ def add_client(): return redirect(url_for('clients.clients')) return render_template('add_client.html') -# Client detail page with all four sections @clients_bp.route('/clients/') def client_detail(client_id): client = Client.query.get_or_404(client_id) - # For the Domains dropdown: only available non-SSL-only domains. available_domains = Domain.query.filter_by(ssl_only=False).all() - # For the SSL dropdown: only available SSL-only domains. available_ssl = Domain.query.filter_by(ssl_only=True).all() + available_categories = PasswordCategory.query.all() + # Group passwords by category (or "Uncategorized" if none) + passwords_by_category = {} + for p in client.passwords: + cat = p.category.name if p.category else "Uncategorized" + if cat not in passwords_by_category: + passwords_by_category[cat] = [] + passwords_by_category[cat].append(p) return render_template('client_detail.html', client=client, available_domains=available_domains, - available_ssl=available_ssl) + available_ssl=available_ssl, + available_categories=available_categories, + passwords_by_category=passwords_by_category) -# Update client details (phone, address) @clients_bp.route('/clients//update_details', methods=['POST']) def update_client_details(client_id): client = Client.query.get_or_404(client_id) @@ -44,20 +48,21 @@ def update_client_details(client_id): db.session.commit() return redirect(url_for('clients.client_detail', client_id=client.id)) -# Add a new password for the client @clients_bp.route('/clients//add_password', methods=['POST']) def add_client_password(client_id): client = Client.query.get_or_404(client_id) item = request.form.get('item') username = request.form.get('username') - password = request.form.get('password') - if item and username and password: - new_pass = ClientPassword(item=item, username=username, password=password, client_id=client.id) + password_text = request.form.get('password') + category_id = request.form.get('category_id') + if item and username and password_text: + new_pass = ClientPassword(item=item, username=username, password=password_text, client_id=client.id) + if category_id: + new_pass.category_id = int(category_id) db.session.add(new_pass) db.session.commit() return redirect(url_for('clients.client_detail', client_id=client.id)) -# Attach a domain (non SSL-only) to the client @clients_bp.route('/clients//attach_domain', methods=['POST']) def attach_domain(client_id): client = Client.query.get_or_404(client_id) @@ -69,7 +74,6 @@ def attach_domain(client_id): db.session.commit() return redirect(url_for('clients.client_detail', client_id=client.id)) -# Attach an SSL domain (ssl_only) to the client @clients_bp.route('/clients//attach_ssl', methods=['POST']) def attach_ssl(client_id): client = Client.query.get_or_404(client_id) @@ -81,7 +85,7 @@ def attach_ssl(client_id): db.session.commit() return redirect(url_for('clients.client_detail', client_id=client.id)) - +# Removal routes for associations @clients_bp.route('/clients//remove_domain/', methods=['POST']) def remove_domain(client_id, domain_id): client = Client.query.get_or_404(client_id) @@ -106,3 +110,11 @@ def remove_password(client_id, password_id): db.session.delete(password) db.session.commit() return redirect(url_for('clients.client_detail', client_id=client_id)) + +@clients_bp.route('/clients//delete', methods=['POST']) +def delete_client(client_id): + client = Client.query.get_or_404(client_id) + db.session.delete(client) + db.session.commit() + flash("Client deleted successfully.", "success") + return redirect(url_for('clients.clients')) \ No newline at end of file diff --git a/app/routes/tools.py b/app/routes/tools.py index c47056b..cfacc25 100644 --- a/app/routes/tools.py +++ b/app/routes/tools.py @@ -1,6 +1,6 @@ import json from flask import Blueprint, render_template, request, redirect, url_for, flash, Response -from app.models import Domain +from app.models import Domain, PasswordCategory from app import db tools_bp = Blueprint('tools', __name__) @@ -11,7 +11,6 @@ def tools(): @tools_bp.route('/tools/export', methods=['GET']) def export_data(): - # Query all domain data from the database. domains = Domain.query.all() data = [] for d in domains: @@ -23,7 +22,6 @@ def export_data(): 'ssl_only': d.ssl_only }) json_data = json.dumps(data) - # Create a response that prompts the user to download the JSON file. response = Response(json_data, mimetype='application/json') response.headers['Content-Disposition'] = 'attachment; filename=domains_data.json' return response @@ -35,7 +33,6 @@ def import_data(): if file: try: data = json.load(file) - # Insert each record if the domain doesn't already exist. for record in data: existing = Domain.query.filter_by(domain=record.get('domain')).first() if not existing: @@ -54,3 +51,39 @@ def import_data(): flash("No file uploaded", "warning") return redirect(url_for('tools.tools')) return render_template('tools.html') + +@tools_bp.route('/tools/password_categories', methods=['GET', 'POST']) +def password_categories(): + if request.method == 'POST': + category_name = request.form.get('category_name') + if category_name: + existing = PasswordCategory.query.filter_by(name=category_name.strip()).first() + if not existing: + new_cat = PasswordCategory(name=category_name.strip()) + db.session.add(new_cat) + db.session.commit() + categories = PasswordCategory.query.all() + return render_template('password_categories.html', categories=categories) + +@tools_bp.route('/tools/password_categories/delete/', methods=['POST']) +def delete_password_category(category_id): + from app.models import ClientPassword, PasswordCategory + # Retrieve the category to be deleted + cat = PasswordCategory.query.get_or_404(category_id) + # Do not allow deletion of the default "General" category + if cat.name == "General": + flash("Cannot delete the default category.", "warning") + return redirect(url_for('tools.password_categories')) + # Find the default "General" category or create it if it doesn't exist + default_cat = PasswordCategory.query.filter_by(name="General").first() + if not default_cat: + default_cat = PasswordCategory(name="General") + db.session.add(default_cat) + db.session.commit() + # Reassign all passwords using the category to be deleted to "General" + ClientPassword.query.filter_by(category_id=cat.id).update({"category_id": default_cat.id}) + # Delete the category + db.session.delete(cat) + db.session.commit() + flash("Category deleted and associated passwords reassigned to General.", "success") + return redirect(url_for('tools.password_categories')) diff --git a/app/templates/client_detail.html b/app/templates/client_detail.html index d17b679..d0e7af0 100644 --- a/app/templates/client_detail.html +++ b/app/templates/client_detail.html @@ -40,30 +40,38 @@

Passwords

- - - - - - - - - - - {% for p in client.passwords %} - - - - - - - {% endfor %} - -
ItemUsernamePasswordAction
{{ p.item }}{{ p.username }}{{ p.password }} -
- -
-
+ {% for category, passwords in passwords_by_category.items() %} + {% if passwords|length > 0 %} +

{{ category }}

+ + + + + + + + + + + {% for p in passwords %} + + + + + + + {% endfor %} + +
ItemUsernamePasswordAction
{{ p.item }}{{ p.username }}{{ p.password }} +
+ +
+
+ {% endif %} + {% endfor %} @@ -82,6 +90,19 @@
+
+ + +
+ @@ -108,7 +129,10 @@ {{ d.expiration_date }}
- +
@@ -119,6 +143,7 @@
+ {% for d in available_ssl %} {% endfor %} @@ -167,4 +196,13 @@
+ +
+ + +
+
+ +
+
{% endblock %} diff --git a/app/templates/password_categories.html b/app/templates/password_categories.html new file mode 100644 index 0000000..987af5e --- /dev/null +++ b/app/templates/password_categories.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% block title %}Password Categories{% endblock %} +{% block content %} +
+

Password Categories

+
+
+ + +
+ +
+ {% if categories %} + + + + + + + + + {% for cat in categories %} + + + + + {% endfor %} + +
Category NameAction
{{ cat.name }} + {% if cat.name != 'General' %} +
+ +
+ {% else %} + Default + {% endif %} +
+ {% else %} +

No categories defined.

+ {% endif %} +
+{% endblock %} diff --git a/app/templates/ssl.html b/app/templates/ssl.html index 654a31d..8a7ddf0 100644 --- a/app/templates/ssl.html +++ b/app/templates/ssl.html @@ -25,9 +25,9 @@ {{ d.ssl_expiration_date }} {% if d.ssl_only %} - SSL Only + Email {% else %} - Normal + Domain {% endif %} diff --git a/app/templates/tools.html b/app/templates/tools.html index 7e18f90..c6208e8 100644 --- a/app/templates/tools.html +++ b/app/templates/tools.html @@ -3,10 +3,8 @@ {% block content %}

Tools

-
-
- Import / Export Data -
+
+
Import / Export Data
@@ -22,5 +20,11 @@
+
+
Password Categories
+ +
{% endblock %}