Passwords Working

main
Bryan Cerrati 2025-03-07 11:00:27 -05:00
parent 1c83e211e7
commit a569ac5f14
8 changed files with 224 additions and 63 deletions

View File

@ -1,6 +1,9 @@
import os import os
from flask import Flask from flask import Flask
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import event
from sqlalchemy.engine import Engine
db = SQLAlchemy() db = SQLAlchemy()
@ -17,10 +20,22 @@ def create_app():
# Initialize SQLAlchemy with the app # Initialize SQLAlchemy with the app
db.init_app(app) db.init_app(app)
# Enable SQLite foreign key support.
with app.app_context(): with app.app_context():
# Import models so that they are registered with SQLAlchemy before creating tables
from app import models from app import models
db.create_all() 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 # Register blueprints
from app.routes.domain import domain_bp from app.routes.domain import domain_bp

View File

@ -1,25 +1,25 @@
from app import db from app import db
# Association tables for many-to-many relationships. # Association tables (if not already defined)
client_domains = db.Table('client_domains', 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')) db.Column('domain_id', db.Integer, db.ForeignKey('domain.id'))
) )
client_ssl = db.Table('client_ssl', 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')) db.Column('domain_id', db.Integer, db.ForeignKey('domain.id'))
) )
class Domain(db.Model): class Domain(db.Model):
__tablename__ = 'domain' __tablename__ = 'domain'
__table_args__ = {'extend_existing': True} __table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
domain = db.Column(db.String(255), unique=True, nullable=False) domain = db.Column(db.String(255), unique=True, nullable=False)
expiration_date = db.Column(db.String(255)) # WHOIS expiration date expiration_date = db.Column(db.String(255)) # WHOIS expiration date
ssl_expiration_date = db.Column(db.String(255)) # SSL certificate 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): def __repr__(self):
return f"<Domain {self.domain} | Whois: {self.expiration_date} | SSL: {self.ssl_expiration_date} | ssl_only: {self.ssl_only}>" return f"<Domain {self.domain} | Whois: {self.expiration_date} | SSL: {self.ssl_expiration_date} | ssl_only: {self.ssl_only}>"
@ -31,15 +31,28 @@ class Client(db.Model):
phone = db.Column(db.String(255)) phone = db.Column(db.String(255))
address = db.Column(db.String(255)) address = db.Column(db.String(255))
# Relationship for storing passwords # Delete all passwords when the client is deleted.
passwords = db.relationship('ClientPassword', backref='client', lazy=True) passwords = db.relationship('ClientPassword', backref='client', lazy=True,
# Many-to-many relationships for domains and SSL domains cascade="all, delete-orphan")
domains = db.relationship('Domain', secondary=client_domains, backref='clients')
ssl_domains = db.relationship('Domain', secondary=client_ssl, backref='ssl_clients') # 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): def __repr__(self):
return f"<Client {self.name}>" return f"<Client {self.name}>"
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"<PasswordCategory {self.name}>"
class ClientPassword(db.Model): class ClientPassword(db.Model):
__tablename__ = 'client_password' __tablename__ = 'client_password'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -47,6 +60,8 @@ class ClientPassword(db.Model):
username = db.Column(db.String(255), nullable=False) username = db.Column(db.String(255), nullable=False)
password = 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) 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): def __repr__(self):
return f"<ClientPassword {self.item}>" return f"<ClientPassword {self.item}>"

View File

@ -1,16 +1,14 @@
from flask import Blueprint, render_template, request, redirect, url_for from flask import Blueprint, render_template, request, redirect, url_for, flash
from app.models import Client, ClientPassword, Domain from app.models import Client, ClientPassword, Domain, PasswordCategory
from app import db from app import db
clients_bp = Blueprint('clients', __name__) clients_bp = Blueprint('clients', __name__)
# List all clients
@clients_bp.route('/clients') @clients_bp.route('/clients')
def clients(): def clients():
clients_list = Client.query.all() clients_list = Client.query.all()
return render_template('clients.html', clients=clients_list) return render_template('clients.html', clients=clients_list)
# Add multiple clients at once
@clients_bp.route('/clients/add', methods=['GET', 'POST']) @clients_bp.route('/clients/add', methods=['GET', 'POST'])
def add_client(): def add_client():
if request.method == 'POST': if request.method == 'POST':
@ -23,19 +21,25 @@ def add_client():
return redirect(url_for('clients.clients')) return redirect(url_for('clients.clients'))
return render_template('add_client.html') return render_template('add_client.html')
# Client detail page with all four sections
@clients_bp.route('/clients/<int:client_id>') @clients_bp.route('/clients/<int:client_id>')
def client_detail(client_id): def client_detail(client_id):
client = Client.query.get_or_404(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() 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_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, return render_template('client_detail.html', client=client,
available_domains=available_domains, 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/<int:client_id>/update_details', methods=['POST']) @clients_bp.route('/clients/<int:client_id>/update_details', methods=['POST'])
def update_client_details(client_id): def update_client_details(client_id):
client = Client.query.get_or_404(client_id) client = Client.query.get_or_404(client_id)
@ -44,20 +48,21 @@ def update_client_details(client_id):
db.session.commit() db.session.commit()
return redirect(url_for('clients.client_detail', client_id=client.id)) return redirect(url_for('clients.client_detail', client_id=client.id))
# Add a new password for the client
@clients_bp.route('/clients/<int:client_id>/add_password', methods=['POST']) @clients_bp.route('/clients/<int:client_id>/add_password', methods=['POST'])
def add_client_password(client_id): def add_client_password(client_id):
client = Client.query.get_or_404(client_id) client = Client.query.get_or_404(client_id)
item = request.form.get('item') item = request.form.get('item')
username = request.form.get('username') username = request.form.get('username')
password = request.form.get('password') password_text = request.form.get('password')
if item and username and password: category_id = request.form.get('category_id')
new_pass = ClientPassword(item=item, username=username, password=password, client_id=client.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.add(new_pass)
db.session.commit() db.session.commit()
return redirect(url_for('clients.client_detail', client_id=client.id)) return redirect(url_for('clients.client_detail', client_id=client.id))
# Attach a domain (non SSL-only) to the client
@clients_bp.route('/clients/<int:client_id>/attach_domain', methods=['POST']) @clients_bp.route('/clients/<int:client_id>/attach_domain', methods=['POST'])
def attach_domain(client_id): def attach_domain(client_id):
client = Client.query.get_or_404(client_id) client = Client.query.get_or_404(client_id)
@ -69,7 +74,6 @@ def attach_domain(client_id):
db.session.commit() db.session.commit()
return redirect(url_for('clients.client_detail', client_id=client.id)) return redirect(url_for('clients.client_detail', client_id=client.id))
# Attach an SSL domain (ssl_only) to the client
@clients_bp.route('/clients/<int:client_id>/attach_ssl', methods=['POST']) @clients_bp.route('/clients/<int:client_id>/attach_ssl', methods=['POST'])
def attach_ssl(client_id): def attach_ssl(client_id):
client = Client.query.get_or_404(client_id) client = Client.query.get_or_404(client_id)
@ -81,7 +85,7 @@ def attach_ssl(client_id):
db.session.commit() db.session.commit()
return redirect(url_for('clients.client_detail', client_id=client.id)) return redirect(url_for('clients.client_detail', client_id=client.id))
# Removal routes for associations
@clients_bp.route('/clients/<int:client_id>/remove_domain/<int:domain_id>', methods=['POST']) @clients_bp.route('/clients/<int:client_id>/remove_domain/<int:domain_id>', methods=['POST'])
def remove_domain(client_id, domain_id): def remove_domain(client_id, domain_id):
client = Client.query.get_or_404(client_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.delete(password)
db.session.commit() db.session.commit()
return redirect(url_for('clients.client_detail', client_id=client_id)) return redirect(url_for('clients.client_detail', client_id=client_id))
@clients_bp.route('/clients/<int:client_id>/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'))

View File

@ -1,6 +1,6 @@
import json import json
from flask import Blueprint, render_template, request, redirect, url_for, flash, Response 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 from app import db
tools_bp = Blueprint('tools', __name__) tools_bp = Blueprint('tools', __name__)
@ -11,7 +11,6 @@ def tools():
@tools_bp.route('/tools/export', methods=['GET']) @tools_bp.route('/tools/export', methods=['GET'])
def export_data(): def export_data():
# Query all domain data from the database.
domains = Domain.query.all() domains = Domain.query.all()
data = [] data = []
for d in domains: for d in domains:
@ -23,7 +22,6 @@ def export_data():
'ssl_only': d.ssl_only 'ssl_only': d.ssl_only
}) })
json_data = json.dumps(data) 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 = Response(json_data, mimetype='application/json')
response.headers['Content-Disposition'] = 'attachment; filename=domains_data.json' response.headers['Content-Disposition'] = 'attachment; filename=domains_data.json'
return response return response
@ -35,7 +33,6 @@ def import_data():
if file: if file:
try: try:
data = json.load(file) data = json.load(file)
# Insert each record if the domain doesn't already exist.
for record in data: for record in data:
existing = Domain.query.filter_by(domain=record.get('domain')).first() existing = Domain.query.filter_by(domain=record.get('domain')).first()
if not existing: if not existing:
@ -54,3 +51,39 @@ def import_data():
flash("No file uploaded", "warning") flash("No file uploaded", "warning")
return redirect(url_for('tools.tools')) return redirect(url_for('tools.tools'))
return render_template('tools.html') 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/<int:category_id>', 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'))

View File

@ -40,30 +40,38 @@
<!-- Section 2: Passwords --> <!-- Section 2: Passwords -->
<div class="mt-4"> <div class="mt-4">
<h2 class="section-title">Passwords</h2> <h2 class="section-title">Passwords</h2>
<table class="table table-bordered table-hover"> {% for category, passwords in passwords_by_category.items() %}
<thead class="thead-light"> {% if passwords|length > 0 %}
<tr> <h3 class="mt-3">{{ category }}</h3>
<th>Item</th> <table class="table table-bordered table-hover">
<th>Username</th> <thead class="thead-light">
<th>Password</th> <tr>
<th>Action</th> <th>Item</th>
</tr> <th>Username</th>
</thead> <th>Password</th>
<tbody> <th>Action</th>
{% for p in client.passwords %} </tr>
<tr> </thead>
<td>{{ p.item }}</td> <tbody>
<td>{{ p.username }}</td> {% for p in passwords %}
<td>{{ p.password }}</td> <tr>
<td> <td>{{ p.item }}</td>
<form action="{{ url_for('clients.remove_password', client_id=client.id, password_id=p.id) }}" method="post" class="d-inline"> <td>{{ p.username }}</td>
<button type="submit" class="btn btn-danger btn-sm">Delete</button> <td>{{ p.password }}</td>
</form> <td>
</td> <form action="{{ url_for('clients.remove_password', client_id=client.id, password_id=p.id) }}" method="post" class="d-inline">
</tr> <button type="submit" class="btn btn-danger btn-sm"
{% endfor %} onclick="return confirm('WARNING: Deleting this password is irreversible. Are you sure you want to delete this password?');">
</tbody> Delete
</table> </button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
<button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#passwordForm" aria-expanded="false" aria-controls="passwordForm"> <button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#passwordForm" aria-expanded="false" aria-controls="passwordForm">
Add Password Add Password
</button> </button>
@ -82,6 +90,19 @@
<label for="password">Password:</label> <label for="password">Password:</label>
<input type="text" class="form-control" name="password" placeholder="Password"> <input type="text" class="form-control" name="password" placeholder="Password">
</div> </div>
<div class="form-group">
<label for="category">Category:</label>
<select name="category_id" class="form-control">
{% for cat in available_categories %}
{% if cat.name == "General" %}
<option value="{{ cat.id }}" selected>{{ cat.name }}</option>
{% else %}
<option value="{{ cat.id }}">{{ cat.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-success">Add Password</button> <button type="submit" class="btn btn-success">Add Password</button>
</form> </form>
</div> </div>
@ -108,7 +129,10 @@
<td>{{ d.expiration_date }}</td> <td>{{ d.expiration_date }}</td>
<td> <td>
<form action="{{ url_for('clients.remove_domain', client_id=client.id, domain_id=d.id) }}" method="post" class="d-inline"> <form action="{{ url_for('clients.remove_domain', client_id=client.id, domain_id=d.id) }}" method="post" class="d-inline">
<button type="submit" class="btn btn-danger btn-sm">Delete</button> <button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('Are you sure you want to delete this domain association?');">
Delete
</button>
</form> </form>
</td> </td>
</tr> </tr>
@ -119,6 +143,7 @@
<div class="form-group mb-2"> <div class="form-group mb-2">
<label for="domain_id" class="sr-only">Select Domain</label> <label for="domain_id" class="sr-only">Select Domain</label>
<select name="domain_id" class="form-control"> <select name="domain_id" class="form-control">
<option value=""> ---- </option>
{% for d in available_domains %} {% for d in available_domains %}
<option value="{{ d.id }}">{{ d.domain }}</option> <option value="{{ d.id }}">{{ d.domain }}</option>
{% endfor %} {% endfor %}
@ -148,7 +173,10 @@
<td>{{ d.ssl_expiration_date }}</td> <td>{{ d.ssl_expiration_date }}</td>
<td> <td>
<form action="{{ url_for('clients.remove_ssl', client_id=client.id, domain_id=d.id) }}" method="post" class="d-inline"> <form action="{{ url_for('clients.remove_ssl', client_id=client.id, domain_id=d.id) }}" method="post" class="d-inline">
<button type="submit" class="btn btn-danger btn-sm">Delete</button> <button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('Are you sure you want to delete this SSL domain association?');">
Delete
</button>
</form> </form>
</td> </td>
</tr> </tr>
@ -159,6 +187,7 @@
<div class="form-group mb-2"> <div class="form-group mb-2">
<label for="ssl_domain_id" class="sr-only">Select SSL Domain</label> <label for="ssl_domain_id" class="sr-only">Select SSL Domain</label>
<select name="ssl_domain_id" class="form-control"> <select name="ssl_domain_id" class="form-control">
<option value=""> ---- </option>
{% for d in available_ssl %} {% for d in available_ssl %}
<option value="{{ d.id }}">{{ d.domain }}</option> <option value="{{ d.id }}">{{ d.domain }}</option>
{% endfor %} {% endfor %}
@ -167,4 +196,13 @@
<button type="submit" class="btn btn-primary mb-2 ml-2">Attach SSL Domain</button> <button type="submit" class="btn btn-primary mb-2 ml-2">Attach SSL Domain</button>
</form> </form>
</div> </div>
<hr>
<!-- Delete Client Section -->
<div class="mt-4">
<form action="{{ url_for('clients.delete_client', client_id=client.id) }}" method="post" onsubmit="return confirm('WARNING: Deleting this client will remove all associated data. Are you sure you want to proceed?');">
<button type="submit" class="btn btn-danger btn-block">Delete Client</button>
</form>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}Password Categories{% endblock %}
{% block content %}
<div class="container">
<h1 class="my-4">Password Categories</h1>
<form method="post" class="form-inline mb-4">
<div class="form-group">
<label for="category_name" class="sr-only">Category Name:</label>
<input type="text" name="category_name" class="form-control" placeholder="Enter category name">
</div>
<button type="submit" class="btn btn-primary ml-2">Add Category</button>
</form>
{% if categories %}
<table class="table table-bordered table-hover">
<thead class="thead-light">
<tr>
<th>Category Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for cat in categories %}
<tr>
<td>{{ cat.name }}</td>
<td>
{% if cat.name != 'General' %}
<form action="{{ url_for('tools.delete_password_category', category_id=cat.id) }}" method="post" class="d-inline">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure you want to delete this category? All passwords will be reassigned to General.');">
Delete
</button>
</form>
{% else %}
<span class="text-muted">Default</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No categories defined.</p>
{% endif %}
</div>
{% endblock %}

View File

@ -25,9 +25,9 @@
<td>{{ d.ssl_expiration_date }}</td> <td>{{ d.ssl_expiration_date }}</td>
<td> <td>
{% if d.ssl_only %} {% if d.ssl_only %}
<span class="badge badge-info">SSL Only</span> <span class="badge badge-info">Email</span>
{% else %} {% else %}
<span class="badge badge-secondary">Normal</span> <span class="badge badge-secondary">Domain</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>

View File

@ -3,10 +3,8 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1 class="my-4">Tools</h1> <h1 class="my-4">Tools</h1>
<div class="card"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">Import / Export Data</div>
Import / Export Data
</div>
<div class="card-body"> <div class="card-body">
<div class="mb-4"> <div class="mb-4">
<form action="{{ url_for('tools.import_data') }}" method="post" enctype="multipart/form-data"> <form action="{{ url_for('tools.import_data') }}" method="post" enctype="multipart/form-data">
@ -22,5 +20,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card">
<div class="card-header">Password Categories</div>
<div class="card-body">
<a href="{{ url_for('tools.password_categories') }}" class="btn btn-info">Manage Password Categories</a>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}