Kaynağa Gözat

Added Admin and ILS user management. Still need to create edit forms.

Adam Day 2 yıl önce
ebeveyn
işleme
e9d4547d01

+ 1 - 1
.gitignore

@@ -3,4 +3,4 @@
 /app.spec
 /build/
 /dist/
-/users.db
+/*.db

+ 182 - 9
app.py

@@ -1,25 +1,25 @@
 import datetime as datetime
-import configparser as configparser
-import os
 from urllib.parse import urlparse
 import webbrowser
 from emailtool.emailer import Emailer
-from flask import Flask, render_template, request, redirect, url_for, jsonify, session
-from flask_httpauth import HTTPBasicAuth
+from flask import Flask, render_template, request, redirect, url_for, session, send_file
 import configparser
 from gevent.pywsgi import WSGIServer
 import socket
 import logging
-from peewee import Model, CharField, TextField, DateTimeField, SqliteDatabase, BooleanField
+from peewee import Model, CharField, DateTimeField, SqliteDatabase, BooleanField
 import hashlib
+import os
+import csv
 
-db_name = 'users.db'
+db_name = 'app.db'
 db = SqliteDatabase(db_name)
 
 
 class User(Model):
     username = CharField()
     password = CharField()
+    date_created = DateTimeField(default=datetime.datetime.now)
     logged_in = BooleanField(default=False)
 
     class Meta:
@@ -97,6 +97,7 @@ def is_valid_url(url):
     except:
         return False
 
+
 def requires_auth():
     if 'username' in session:
         username = session['username']
@@ -112,7 +113,8 @@ def requires_auth():
 # Check for DB tables and create if they don't exist
 if db.table_exists('user') is False:
     db.create_tables([User, IlsUser])
-    User.create(username='admin', password=encrypt_password('admin'))
+    User.create(username='admin', password=encrypt_password('admin'),
+                date_created=datetime.datetime.now(), logged_in=False).save()
 
 if db.table_exists('ilsuser') is False:
     db.create_tables([IlsUser])
@@ -122,6 +124,7 @@ db.close()
 app = Flask(__name__)
 app.secret_key = 'super secret key'
 
+
 @app.before_request
 def before_request():
     db.connect()
@@ -179,7 +182,7 @@ def index():
 
 
 # Create a route for admin page
-@app.route('/admin')
+@app.route('/admin/')
 def admin():
     # Check to see if user is logged in
     if not requires_auth():
@@ -188,6 +191,177 @@ def admin():
     return render_template('admin.html')
 
 
+@app.route('/admin/users/', methods=['GET', 'POST'])
+def admin_users():
+    # Check to see if user is logged in
+    if not requires_auth():
+        return redirect(url_for('login'))
+    message = None
+    if request.method == 'POST':
+        username = request.form.get('username')
+        password = request.form.get('password')
+        confirm_password = request.form.get('confirm_password')
+
+        # Check to see if username already exists
+        try:
+            user = User.filter(User.username == username).first()
+        except Exception as e:
+            print(e)
+            user = None
+
+        if user:
+            message = 'Username already exists'
+        else:
+            if password == confirm_password:
+                User.create(username=username, password=encrypt_password(password),
+                            date_created=datetime.datetime.now(), logged_in=False).save()
+                message = 'User created successfully'
+            else:
+                message = 'Passwords do not match'
+
+    # Get all admin users from the DB
+    users = User.select().execute()
+
+    context = {
+        'users': users,
+        'message': message,
+    }
+
+    return render_template('admin_users.html', context=context)
+
+
+@app.route('/admin/users/delete/<int:id>', methods=['GET', 'POST'])
+def admin_users_delete(id):
+    # Check to see if user is logged in
+    if not requires_auth():
+        return redirect(url_for('login'))
+
+    # Get the user from the DB
+    user = User.get(User.id == id)
+
+    if user.username != 'admin':
+        # Unset session variable if the user is deleting their own account
+        if user.username == session['username']:
+            session.pop('username', None)
+        user.delete_instance()
+
+    return redirect(url_for('admin_users'))
+
+
+@app.route('/admin/users/ils', methods=['GET', 'POST'])
+def admin_ils_users():
+    # Check to see if user is logged in
+    if not requires_auth():
+        return redirect(url_for('login'))
+    message = None
+    if request.method == 'POST':
+        username = request.form.get('username')
+        email = request.form.get('email')
+
+        # Check to see if username already exists
+        try:
+            user = IlsUser.filter(IlsUser.username == username).first()
+        except Exception as e:
+            print(e)
+            user = None
+
+        if user:
+            message = 'Username already exists'
+        else:
+
+            IlsUser.create(username=username, email=email, reset_datetime=datetime.datetime.now()).save()
+            message = 'User created successfully'
+
+    # Get all admin users from the DB
+    users = IlsUser.select().execute()
+
+    context = {
+        'users': users,
+        'message': message,
+    }
+
+    return render_template('admin_ils_users.html', context=context)
+
+
+@app.route('/admin/users/ils/delete/<int:id>', methods=['GET', 'POST'])
+def admin_ils_users_delete(id):
+    # Check to see if user is logged in
+    if not requires_auth():
+        return redirect(url_for('login'))
+
+    # Get the user from the DB
+    user = IlsUser.get(IlsUser.id == id)
+    user.delete_instance()
+
+    return redirect(url_for('admin_ils_users'))
+
+
+# create a route for generating a CSV file for download
+@app.route('/admin/users/ils/csv/download', methods=['GET', 'POST'])
+def admin_ils_users_csv_download():
+    # Check to see if user is logged in
+    if not requires_auth():
+        return redirect(url_for('login'))
+    # Create a CSV file with the users and don't add a blank line between rows
+    with open('users.csv', 'w', newline='') as f:
+        writer = csv.writer(f)
+        writer.writerow(['username', 'email'])
+        users = IlsUser.select().execute()
+        for user in users:
+            writer.writerow([user.username, user.email])
+
+    # return the CSV file to the user
+    return send_file('users.csv', as_attachment=True)
+
+
+@app.route('/admin/users/ils/csv/import', methods=['GET', 'POST'])
+def admin_ils_users_csv_import():
+    # Check to see if user is logged in
+    if not requires_auth():
+        return redirect(url_for('login'))
+    message = None
+    if request.method == 'POST':
+        csv_file = request.files['csv']
+        if csv_file.filename != '':
+            csv_file.save(os.path.join('uploads', csv_file.filename))
+            with open(os.path.join('uploads', csv_file.filename), 'r') as f:
+                reader = csv.reader(f)
+                for row in reader:
+                    username = row[0]
+                    email = row[1]
+
+                    # ignore the header row
+                    if username == 'username':
+                        continue
+
+                    # ignore blank rows
+                    if username == '':
+                        continue
+
+                    # Check if user already exists and if it does update the entry
+                    try:
+                        user = IlsUser.filter(IlsUser.username == username).first()
+                    except Exception as e:
+                        print(e)
+                        user = None
+
+                    if user:
+                        user.email = email
+                        user.reset_datetime = datetime.datetime.now()
+                        user.save()
+                    else:
+                        IlsUser.create(username=username, email=email, reset_datetime=datetime.datetime.now()).save()
+            # Delete the uploaded file
+            os.remove(os.path.join('uploads', csv_file.filename))
+            return redirect(url_for('admin_ils_users'))
+
+    context = {
+        'message': message,
+    }
+
+    return render_template('csv.html', context=context)
+
+
 @app.route('/logout')
 def logout():
     if 'username' in session:
@@ -237,7 +411,6 @@ def login():
 #@app.teardown_appcontext
 
 
-
 if __name__ == "__main__":
     print("------------------------- Start up -----------------------------")
     print("Starting HTTP Service on port %s..." % http_settings['port'])

+ 2 - 2
templates/admin.html

@@ -1,8 +1,8 @@
-{% extends 'layout.html' %}
+{% extends 'auth_layout.html' %}
 {% block content %}
 <div class="row">
     <div class="col">
-        Admin page
+        <h3><i class="ri-dashboard-fill"></i> Admin Dashboard</h3>
     </div>
 </div>
 {% endblock %}

+ 92 - 0
templates/admin_ils_users.html

@@ -0,0 +1,92 @@
+{% extends 'auth_layout.html' %}
+{% block content %}
+{% if context.message %}
+    <div class="row">
+        <div class="col text-center text-primary">
+            <i class="ri-error-warning-fill"></i> {{ context.message }}
+        </div>
+    </div>
+{% endif %}
+<div class="row">
+    <div class="col">
+        <a href="{{ url_for('admin_ils_users_csv_import') }}" class="btn btn-dark float-end"><i class="ri-file-excel-line"></i> CSV Import</a>
+        <a href="#!" data-bs-toggle="modal" data-bs-target="#add-user" class="btn btn-primary float-end me-2"><i class="ri-user-add-line"></i> Add ILS User</a>
+        <h3><i class="ri-shield-user-fill"></i> ILS Users</h3>
+        <p class="lead">ILS users accounts establish with the ILS system.</p>
+    </div>
+</div>
+<div class="row">
+    <div class="col">
+        <table class="table table-flush">
+            <thead>
+                <tr>
+                    <th scope="col">Username</th>
+                    <th scope="col">Email</th>
+                    <th scope="col">Reset Date</th>
+                    <th scope="col">Actions</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for user in context.users %}
+                <tr>
+                    <td>{{ user.username }}</td>
+                    <td>{{ user.email }}</td>
+                    <td>{{ user.reset_datetime }}</td>
+                    <td>
+                        <a href="" class="btn btn-primary">Edit</a>
+                        <a href="#!" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#remove-user-{{ loop.index0 }}">Delete</a>
+                    </td>
+                </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+    </div>
+</div>
+
+<div class="modal fade" id="add-user" tabindex="-1">
+    <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content">
+            <div class="modal-header bg-light">
+                <h1 class="modal-title fs-5"><i class="ri-user-add-fill"></i> Add ILS User</h1>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <form action="" method="post">
+                <div class="modal-body">
+                    <div class="mb-3">
+                        <input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
+                    </div>
+                    <div class="mb-3">
+                        <input type="email" class="form-control" id="email" name="email" placeholder="Email" required>
+                    </div>
+                </div>
+                <div class="modal-footer bg-light">
+                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
+                    <input type="submit" class="btn btn-primary" value="Save">
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+
+{% for user in context.users %}
+<div class="modal fade" id="remove-user-{{ loop.index0 }}" tabindex="-1">
+    <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content">
+            <div class="modal-header bg-light">
+                <h1 class="modal-title fs-5">Confirmation</h1>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <div class="modal-body">
+                <div class="mb-3">
+                    <p class="lead">Are you sure you want to delete this user?</p>
+                </div>
+            </div>
+            <div class="modal-footer bg-light">
+                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
+                <a class="btn btn-danger" href="{{ url_for('admin_ils_users_delete', id=user.id) }}">Yes, remove the user</a>
+            </div>
+        </div>
+    </div>
+</div>
+{% endfor %}
+{% endblock %}

+ 100 - 0
templates/admin_users.html

@@ -0,0 +1,100 @@
+{% extends 'auth_layout.html' %}
+{% block content %}
+{% if context.message %}
+    <div class="row">
+        <div class="col text-center text-primary">
+            <i class="ri-error-warning-fill"></i> {{ context.message }}
+        </div>
+    </div>
+{% endif %}
+<div class="row">
+    <div class="col">
+        <a href="#!" data-bs-toggle="modal" data-bs-target="#add-user" class="btn btn-primary float-end"><i class="ri-user-add-line"></i> Add Admin User</a>
+        <h3><i class="ri-shield-user-line"></i> Admin Users</h3>
+        <p class="lead">Admin Users are users that can modify this tool.</p>
+    </div>
+</div>
+<div class="row">
+    <div class="col">
+        <table class="table table-flush">
+            <thead>
+                <tr>
+                    <th scope="col">Username</th>
+                    <th scope="col">Email</th>
+                    <th scope="col">Create Date</th>
+                    <th scope="col">Login Status</th>
+                    <th scope="col">Actions</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for user in context.users %}
+                <tr>
+                    <td>{{ user.username }}</td>
+                    <td>{{ user.email }}</td>
+                    <td>{{ user.date_created }}</td>
+                    <td>{{ user.logged_in }}</td>
+                    <td>
+                        <a href="" class="btn btn-primary">Edit</a>
+                        <a href="#!" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#remove-user-{{ loop.index0 }}">Delete</a>
+                    </td>
+                </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+    </div>
+</div>
+
+<div class="modal fade" id="add-user" tabindex="-1">
+    <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content">
+            <div class="modal-header bg-light">
+                <h1 class="modal-title fs-5"><i class="ri-user-add-line"></i> Add Admin User</h1>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <form action="" method="post">
+                <div class="modal-body">
+                    <div class="mb-3">
+                        <input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
+                    </div>
+                    <div class="mb-3">
+                        <input type="email" class="form-control" id="email" name="email" placeholder="Email" required>
+                    </div>
+                    <div class="mb-3">
+                        <input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
+                    </div>
+                    <div class="mb-3">
+                        <input type="password" class="form-control" id="confirm_password" name="confirm_password" placeholder="Confirm Password" required>
+                    </div>
+                </div>
+                <div class="modal-footer bg-light">
+                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
+                    <input type="submit" class="btn btn-primary" value="Save">
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+
+{% for user in context.users %}
+<div class="modal fade" id="remove-user-{{ loop.index0 }}" tabindex="-1">
+    <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content">
+            <div class="modal-header bg-light">
+                <h1 class="modal-title fs-5">Confirmation</h1>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <div class="modal-body">
+                <div class="mb-3">
+                    <p class="lead">Are you sure you want to delete this user?</p>
+                </div>
+            </div>
+            <div class="modal-footer bg-light">
+                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
+                <a class="btn btn-danger" href="{{ url_for('admin_users_delete', id=user.id) }}">Yes, remove the user</a>
+            </div>
+        </div>
+    </div>
+</div>
+{% endfor %}
+
+{% endblock %}

+ 23 - 14
templates/auth_layout.html

@@ -4,31 +4,40 @@
     <meta charset="UTF-8">
     <title>ILS Password Manager</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap-5.3.0-alpha1-dist/css/bootstrap.css') }}">
+    <link rel="stylesheet" href="{{ url_for('static', filename='RemixIcon_Fonts_v2.5.0/fonts/remixicon.css') }}">
     <script src="{{ url_for('static', filename='bootstrap-5.3.0-alpha1-dist/js/bootstrap.bundle.js') }}"></script>
 </head>
-<body>
-<div class="container">
-    <div class="row mt-5">
-        <div class="col-12 text-center">
-            <h1>ILS Password Manager</h1>
+<body class="bg-secondary">
+    <div class="container">
+        <div class="row mt-5">
+            <div class="col-12 text-center">
+                <h1 class="text-light">ILS Password Manager</h1>
+            </div>
         </div>
-        <div class="col-12">
-            <div class="border rounded shadow mb-3">
-                <ul class="nav nav-pills nav-fill">
+        <div class="row mt-5 g-0 shadow bg-white">
+            <div class="col-2 border" style="min-height:50vh;">
+                <ul class="nav flex-column">
+                    <li class="nav-item">
+                        <a class="nav-link bg-light text-dark border-bottom" href="/admin"><i class="ri-dashboard-fill"></i> Admin Dashboard</a>
+                    </li>
                     <li class="nav-item">
-                        <a class="nav-link rounded-end-0" aria-current="page" href="#">Dashboard</a>
+                        <a class="nav-link bg-light text-dark border-bottom" href="/admin/users"><i class="ri-shield-user-line"></i> Admin Users</a>
                     </li>
                     <li class="nav-item">
-                        <a class="nav-link rounded-0" href="#">Accounts</a>
+                        <a class="nav-link bg-light text-dark border-bottom" href="/admin/users/ils"><i class="ri-shield-user-fill"></i> ILS Users</a>
                     </li>
-                    <li class="nav-item ">
-                        <a class="nav-link rounded-start-0" href="#">Settings</a>
+                    <li class="nav-item">
+                        <a class="nav-link bg-light text-dark border-bottom" href="#"><i class="ri-settings-5-line"></i> Settings</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link bg-light text-dark border-bottom" href="/logout"><i class="ri-logout-box-line"></i> Logout</a>
                     </li>
                 </ul>
             </div>
+            <div class="col-10 border-top border-end border-bottom p-3">
+                {% block content %}{% endblock %}
+            </div>
         </div>
     </div>
-    {% block content %}{% endblock %}
-</div>
 </body>
 </html>

+ 37 - 0
templates/csv.html

@@ -0,0 +1,37 @@
+{% extends 'auth_layout.html' %}
+{% block content %}
+{% if context.message %}
+    <div class="row">
+        <div class="col text-center text-primary">
+            <i class="ri-error-warning-fill"></i> {{ context.message }}
+        </div>
+    </div>
+{% endif %}
+
+<div class="row">
+    <div class="col">
+        <a href="{{ url_for('admin_ils_users_csv_download') }}" class="btn btn-secondary float-end"><i class="ri-file-download-fill"></i> Download CSV Template</a>
+        <a href="{{ url_for('admin_ils_users') }}" class="btn btn-dark float-end me-2"><i class="ri-arrow-left-circle-fill"></i> Back</a>
+        <h3><i class="ri-file-excel-line"></i> CSV Import ILS Users</h3>
+        <p class="lead">ILS users can be loaded using a comma delimited CSV file.</p>
+        <p class="alert alert-warning">
+            <i class="ri-error-warning-fill"></i> If users already exist in the database, they will be updated with the information provided in the CSV file.
+        </p>
+    </div>
+</div>
+
+<div class="row">
+    <div class="col">
+        <form action="{{ url_for('admin_ils_users_csv_import') }}" method="post" enctype="multipart/form-data">
+            <div class="mb-3">
+                <label for="csv" class="form-label">CSV File</label>
+                <input type="file" class="form-control" id="csv" name="csv" required>
+            </div>
+            <div class="mb-3">
+                <input type="submit" class="btn btn-primary" value="Import">
+            </div>
+        </form>
+    </div>
+</div>
+
+{% endblock %}

+ 0 - 0
uploads/.gitkeep