Passwords Working
parent
1c83e211e7
commit
a569ac5f14
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"<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))
|
||||
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"<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):
|
||||
__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"<ClientPassword {self.item}>"
|
||||
|
|
|
|||
|
|
@ -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/<int:client_id>')
|
||||
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/<int:client_id>/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/<int:client_id>/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/<int:client_id>/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/<int:client_id>/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/<int:client_id>/remove_domain/<int:domain_id>', 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/<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'))
|
||||
|
|
@ -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/<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'))
|
||||
|
|
|
|||
|
|
@ -40,30 +40,38 @@
|
|||
<!-- Section 2: Passwords -->
|
||||
<div class="mt-4">
|
||||
<h2 class="section-title">Passwords</h2>
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in client.passwords %}
|
||||
<tr>
|
||||
<td>{{ p.item }}</td>
|
||||
<td>{{ p.username }}</td>
|
||||
<td>{{ p.password }}</td>
|
||||
<td>
|
||||
<form action="{{ url_for('clients.remove_password', client_id=client.id, password_id=p.id) }}" method="post" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% for category, passwords in passwords_by_category.items() %}
|
||||
{% if passwords|length > 0 %}
|
||||
<h3 class="mt-3">{{ category }}</h3>
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in passwords %}
|
||||
<tr>
|
||||
<td>{{ p.item }}</td>
|
||||
<td>{{ p.username }}</td>
|
||||
<td>{{ p.password }}</td>
|
||||
<td>
|
||||
<form action="{{ url_for('clients.remove_password', client_id=client.id, password_id=p.id) }}" method="post" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('WARNING: Deleting this password is irreversible. Are you sure you want to delete this password?');">
|
||||
Delete
|
||||
</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">
|
||||
Add Password
|
||||
</button>
|
||||
|
|
@ -82,6 +90,19 @@
|
|||
<label for="password">Password:</label>
|
||||
<input type="text" class="form-control" name="password" placeholder="Password">
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -108,7 +129,10 @@
|
|||
<td>{{ d.expiration_date }}</td>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -119,6 +143,7 @@
|
|||
<div class="form-group mb-2">
|
||||
<label for="domain_id" class="sr-only">Select Domain</label>
|
||||
<select name="domain_id" class="form-control">
|
||||
<option value=""> ---- </option>
|
||||
{% for d in available_domains %}
|
||||
<option value="{{ d.id }}">{{ d.domain }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -148,7 +173,10 @@
|
|||
<td>{{ d.ssl_expiration_date }}</td>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -159,6 +187,7 @@
|
|||
<div class="form-group mb-2">
|
||||
<label for="ssl_domain_id" class="sr-only">Select SSL Domain</label>
|
||||
<select name="ssl_domain_id" class="form-control">
|
||||
<option value=""> ---- </option>
|
||||
{% for d in available_ssl %}
|
||||
<option value="{{ d.id }}">{{ d.domain }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -167,4 +196,13 @@
|
|||
<button type="submit" class="btn btn-primary mb-2 ml-2">Attach SSL Domain</button>
|
||||
</form>
|
||||
</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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -25,9 +25,9 @@
|
|||
<td>{{ d.ssl_expiration_date }}</td>
|
||||
<td>
|
||||
{% if d.ssl_only %}
|
||||
<span class="badge badge-info">SSL Only</span>
|
||||
<span class="badge badge-info">Email</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">Normal</span>
|
||||
<span class="badge badge-secondary">Domain</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@
|
|||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="my-4">Tools</h1>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Import / Export Data
|
||||
</div>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Import / Export Data</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<form action="{{ url_for('tools.import_data') }}" method="post" enctype="multipart/form-data">
|
||||
|
|
@ -22,5 +20,11 @@
|
|||
</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>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in New Issue