|
@@ -10,6 +10,9 @@ from peewee import Model, CharField, DateTimeField, SqliteDatabase, BooleanField
|
|
|
import hashlib
|
|
|
import os
|
|
|
import csv
|
|
|
+import time
|
|
|
+import threading
|
|
|
+import atexit
|
|
|
|
|
|
db_name = 'app.db'
|
|
|
db = SqliteDatabase(db_name)
|
|
@@ -66,6 +69,81 @@ class PasswordResetLog(Model):
|
|
|
database = db
|
|
|
|
|
|
|
|
|
+class EmailReminderLog(Model):
|
|
|
+ date = CharField(default=datetime.datetime.now().strftime('%Y-%m-%d'))
|
|
|
+ email = CharField()
|
|
|
+ interval = CharField()
|
|
|
+ sent = BooleanField(default=False)
|
|
|
+
|
|
|
+ class Meta:
|
|
|
+ database = db
|
|
|
+
|
|
|
+
|
|
|
+def scheduler(stop_event):
|
|
|
+ while not stop_event.is_set():
|
|
|
+ # Get all scheduled intervals from the DB
|
|
|
+ intervals = Schedule.select().execute()
|
|
|
+ users = IlsUser.select().execute()
|
|
|
+
|
|
|
+ # Get settings
|
|
|
+ domain_name_setting = Settings.get(Settings.name == 'Domain Name')
|
|
|
+ domain_name = domain_name_setting.value
|
|
|
+
|
|
|
+ password_reset_interval_setting = Settings.get(Settings.name == 'Password Reset Interval')
|
|
|
+ password_reset_interval = password_reset_interval_setting.value
|
|
|
+
|
|
|
+ smtp_host_setting = Settings.get(Settings.name == 'SMTP Host')
|
|
|
+ smtp_host = smtp_host_setting.value
|
|
|
+
|
|
|
+ smtp_port_setting = Settings.get(Settings.name == 'SMTP Port')
|
|
|
+ smtp_port = smtp_port_setting.value
|
|
|
+
|
|
|
+ smtp_username_setting = Settings.get(Settings.name == 'SMTP Username')
|
|
|
+ smtp_username = smtp_username_setting.value
|
|
|
+
|
|
|
+ smtp_password_setting = Settings.get(Settings.name == 'SMTP Password')
|
|
|
+ smtp_password = smtp_password_setting.value
|
|
|
+
|
|
|
+ # Loop through each user and calculate the number of days until the users password
|
|
|
+ # expires based on the password_reset_interval value.
|
|
|
+ # Loop through all users
|
|
|
+
|
|
|
+ # Schedule emails to be sent for each user and interval
|
|
|
+ for user in users:
|
|
|
+ # get the scheduled intervals
|
|
|
+ for interval in intervals:
|
|
|
+ # Calculate the date (yyyy-mm-dd) that the password will expire and subtract the interval
|
|
|
+ # from that date to get the date that the email should be sent.
|
|
|
+ # Example: If the password reset interval is 90 days and the current date is 2019-01-01
|
|
|
+ # then the password will expire on 2019-04-01. If the interval is 30 days then the
|
|
|
+ # email should be sent on 2019-03-02.
|
|
|
+ # Calculate the date that the password will expire
|
|
|
+ password_expiration_date = user.reset_datetime + datetime.timedelta(days=int(password_reset_interval))
|
|
|
+ # Subtract the interval from the password expiration date to get the date that the email should be sent
|
|
|
+ email_date = password_expiration_date - datetime.timedelta(days=int(interval.interval))
|
|
|
+
|
|
|
+ # check if reminder has already been saved to the DB
|
|
|
+ reminder = EmailReminderLog.select().where(EmailReminderLog.email == user.email, EmailReminderLog.interval == interval.interval).execute()
|
|
|
+ if reminder:
|
|
|
+ continue
|
|
|
+ else:
|
|
|
+ # Store the date in the DB
|
|
|
+ EmailReminderLog.create(date=email_date.strftime('%Y-%m-%d'), email=user.email, interval=interval.interval).save()
|
|
|
+
|
|
|
+ # Get all email reminders from the DB
|
|
|
+ # if today's date matches the EmailReminderLog date then send the email and mark the reminder as sent
|
|
|
+ reminders = EmailReminderLog.select().where(EmailReminderLog.date == datetime.datetime.now().strftime('%Y-%m-%d')).execute()
|
|
|
+ for reminder in reminders:
|
|
|
+ # Send the email
|
|
|
+ emailer = Emailer(smtp_host, smtp_port, smtp_username, smtp_password)
|
|
|
+ emailer.send_email(reminder.email, 'Password Reset Reminder', 'Your password will expire in ' + reminder.interval + ' days. Please visit ' + domain_name + ' to reset your password.')
|
|
|
+ # Mark the reminder as sent
|
|
|
+ reminder.sent = True
|
|
|
+ reminder.save()
|
|
|
+
|
|
|
+ time.sleep(5) # Sleep for 15 minutes
|
|
|
+
|
|
|
+
|
|
|
# Encrypt the password with SHA256
|
|
|
def encrypt_password(password):
|
|
|
hash = hashlib.sha256(password.encode('utf-8')).hexdigest()
|
|
@@ -91,6 +169,7 @@ if db.table_exists('settings') is False:
|
|
|
Settings.create(name='SMTP Password', value="").save()
|
|
|
Settings.create(name='Domain Name', value="lynx").save()
|
|
|
Settings.create(name='Password Reset URL', value="https://terminal.idaho-lynx.org").save()
|
|
|
+ Settings.create(name='Password Reset Interval', value="90").save()
|
|
|
|
|
|
if db.table_exists('log') is False:
|
|
|
db.create_tables([Log, ])
|
|
@@ -101,6 +180,9 @@ if db.table_exists('password_reset_log') is False:
|
|
|
if db.table_exists('schedule') is False:
|
|
|
db.create_tables([Schedule, ])
|
|
|
|
|
|
+if db.table_exists('email_reminder_log') is False:
|
|
|
+ db.create_tables([EmailReminderLog, ])
|
|
|
+
|
|
|
db.close()
|
|
|
|
|
|
settings = Settings.select().execute()
|
|
@@ -111,24 +193,15 @@ debug = debug_setting.value
|
|
|
http_port_setting = Settings.get(Settings.name == 'HTTP Port')
|
|
|
http_port = http_port_setting.value
|
|
|
|
|
|
-smtp_host_setting = Settings.get(Settings.name == 'SMTP Host')
|
|
|
-smtp_host = smtp_host_setting.value
|
|
|
-
|
|
|
-smtp_port_setting = Settings.get(Settings.name == 'SMTP Port')
|
|
|
-smtp_port = smtp_port_setting.value
|
|
|
-
|
|
|
-smtp_username_setting = Settings.get(Settings.name == 'SMTP Username')
|
|
|
-smtp_username = smtp_username_setting.value
|
|
|
-
|
|
|
-smtp_password_setting = Settings.get(Settings.name == 'SMTP Password')
|
|
|
-smtp_password = smtp_password_setting.value
|
|
|
-
|
|
|
domain_name_setting = Settings.get(Settings.name == 'Domain Name')
|
|
|
domain_name = domain_name_setting.value
|
|
|
|
|
|
password_reset_url_setting = Settings.get(Settings.name == 'Password Reset URL')
|
|
|
password_reset_url = password_reset_url_setting.value
|
|
|
|
|
|
+password_reset_interval_setting = Settings.get(Settings.name == 'Password Reset Interval')
|
|
|
+password_reset_interval = password_reset_interval_setting.value
|
|
|
+
|
|
|
log = logging.getLogger('werkzeug')
|
|
|
log.setLevel(logging.INFO)
|
|
|
|
|
@@ -138,18 +211,6 @@ else:
|
|
|
debug = False
|
|
|
|
|
|
|
|
|
-def send_email(to, subject, body):
|
|
|
- # Get settings from smtp_settings
|
|
|
- host = smtp_host
|
|
|
- port = smtp_port
|
|
|
- username = smtp_username
|
|
|
- password = smtp_password
|
|
|
-
|
|
|
- # Create an instance of the Emailer class
|
|
|
- emailer = Emailer(host, port, username, password)
|
|
|
- # Call the send_email method
|
|
|
- emailer.send_email(to, subject, body)
|
|
|
-
|
|
|
# send_email('aday@twinfallspubliclibrary.org', 'TEST', 'This is a test email')
|
|
|
|
|
|
|
|
@@ -202,13 +263,19 @@ def admin_password_check():
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
+# Start the scheduler loop in another thread
|
|
|
+shutdown_scheduler = threading.Event()
|
|
|
+scheduler_thread = threading.Thread(target=scheduler, args=(shutdown_scheduler,))
|
|
|
+scheduler_thread.start()
|
|
|
+
|
|
|
+# Start the HTTP Server
|
|
|
app = Flask(__name__)
|
|
|
app.secret_key = os.urandom(24)
|
|
|
|
|
|
|
|
|
def format_time_ago(timestamp):
|
|
|
"""Calculate the time passed since a datetime stamp and format it as a human-readable string."""
|
|
|
- now = datetime.datetime.utcnow()
|
|
|
+ now = datetime.datetime.now()
|
|
|
diff = now - timestamp
|
|
|
|
|
|
if diff.days > 365:
|
|
@@ -328,7 +395,38 @@ def admin():
|
|
|
if not requires_auth():
|
|
|
return redirect(url_for('login'))
|
|
|
|
|
|
- return render_template('admin.html')
|
|
|
+ # Get a count of all IlsUsers
|
|
|
+ try:
|
|
|
+ ils_users = IlsUser.select().count()
|
|
|
+ except Exception as e:
|
|
|
+ ils_users = None
|
|
|
+
|
|
|
+ # Get a list of coming email reminders for the next 7 days
|
|
|
+ try:
|
|
|
+ email_reminders = PasswordResetLog.select().where(PasswordResetLog.date_created < datetime.datetime.now() + datetime.timedelta(days=7)).execute()
|
|
|
+ except Exception as e:
|
|
|
+ email_reminders = None
|
|
|
+
|
|
|
+ # Get a list ils users that have passwords expiring in the next 7 days
|
|
|
+ try:
|
|
|
+ ils_users_expiring = IlsUser.select().where(IlsUser.password_expires < datetime.datetime.now() + datetime.timedelta(days=7)).execute()
|
|
|
+ except Exception as e:
|
|
|
+ ils_users_expiring = None
|
|
|
+
|
|
|
+ # get a list of email notifications sent in the last 7 days
|
|
|
+ try:
|
|
|
+ # Get a list of all future email reminders
|
|
|
+ email_reminders = EmailReminderLog.select().order_by(EmailReminderLog.date).limit(15).execute()
|
|
|
+ except Exception as e:
|
|
|
+ email_reminders = None
|
|
|
+
|
|
|
+ context = {
|
|
|
+ 'ils_user_count': ils_users,
|
|
|
+ 'ils_users_expiring': ils_users_expiring,
|
|
|
+ 'email_reminders': email_reminders,
|
|
|
+ }
|
|
|
+
|
|
|
+ return render_template('admin.html', context=context)
|
|
|
|
|
|
|
|
|
@app.route('/admin/users/', methods=['GET', 'POST'])
|
|
@@ -387,13 +485,13 @@ def admin_users_edit(id):
|
|
|
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
|
|
|
+ all_users = list()
|
|
|
+ users = User.select().execute()
|
|
|
+ for u in users:
|
|
|
+ if u.username != user.username:
|
|
|
+ all_users.append(u.username)
|
|
|
|
|
|
- if user.username != username:
|
|
|
+ if username in all_users:
|
|
|
message = 'Username already exists'
|
|
|
else:
|
|
|
user.username = username
|
|
@@ -479,11 +577,55 @@ def admin_ils_users_delete(id):
|
|
|
# Get the user from the DB
|
|
|
user = IlsUser.get(IlsUser.id == id)
|
|
|
username = user.username
|
|
|
+
|
|
|
+ # Remove all email reminders for this user
|
|
|
+ reminders = EmailReminderLog.select().where(EmailReminderLog.email == user.email).execute()
|
|
|
+ for reminder in reminders:
|
|
|
+ reminder.delete_instance()
|
|
|
+
|
|
|
user.delete_instance()
|
|
|
Log.create(username=session['username'], action='Removed ILS user: %s' % username, ).save()
|
|
|
return redirect(url_for('admin_ils_users'))
|
|
|
|
|
|
|
|
|
+@app.route('/admin/users/ils/edit/<int:id>', methods=['GET', 'POST'])
|
|
|
+def admin_ils_users_edit(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)
|
|
|
+
|
|
|
+ message = None
|
|
|
+ if request.method == 'POST':
|
|
|
+ username = request.form.get('username')
|
|
|
+ email = request.form.get('email')
|
|
|
+
|
|
|
+ # Check to see if username already exists
|
|
|
+ all_users = list()
|
|
|
+ users = IlsUser.select().execute()
|
|
|
+ for u in users:
|
|
|
+ if u.username != user.username:
|
|
|
+ all_users.append(u.username)
|
|
|
+
|
|
|
+ if username in all_users:
|
|
|
+ message = 'Username already exists'
|
|
|
+ else:
|
|
|
+ user.username = username
|
|
|
+ user.email = email
|
|
|
+ user.save()
|
|
|
+ message = 'User updated successfully'
|
|
|
+ Log.create(username=session['username'], action='Updated ILS user: %s' % username, ).save()
|
|
|
+
|
|
|
+ context = {
|
|
|
+ 'user': user,
|
|
|
+ 'message': message,
|
|
|
+ }
|
|
|
+
|
|
|
+ return render_template('admin_ils_user_edit.html', context=context)
|
|
|
+
|
|
|
+
|
|
|
# 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():
|
|
@@ -572,6 +714,33 @@ def settings():
|
|
|
setting = None
|
|
|
|
|
|
if setting:
|
|
|
+ # Scrub the values based on the setting name
|
|
|
+ if setting.name == 'Debug Mode':
|
|
|
+ if value.lower() == 'true':
|
|
|
+ value = True
|
|
|
+ else:
|
|
|
+ value = False
|
|
|
+ value = bool(value)
|
|
|
+ elif setting.name == 'HTTP Port':
|
|
|
+ value = str(value)
|
|
|
+ elif setting.name == 'SMTP Host':
|
|
|
+ value = str(value)
|
|
|
+ elif setting.name == 'SMTP Port':
|
|
|
+ value = str(value)
|
|
|
+ elif setting.name == 'SMTP Username':
|
|
|
+ value = str(value)
|
|
|
+ elif setting.name == 'SMTP Password':
|
|
|
+ value = str(value)
|
|
|
+ elif setting.name == 'Domain Name':
|
|
|
+ value = str(value)
|
|
|
+ elif setting.name == 'Password Reset URL':
|
|
|
+ value = str(value)
|
|
|
+ elif setting.name == 'Password Reset Interval':
|
|
|
+ if value.isdigit():
|
|
|
+ value = int(value)
|
|
|
+ else:
|
|
|
+ value = setting.value
|
|
|
+
|
|
|
# Update the setting
|
|
|
old_value = setting.value
|
|
|
setting.value = value
|
|
@@ -600,11 +769,22 @@ def schedule():
|
|
|
|
|
|
message = None
|
|
|
|
|
|
+ # Get all schedules from the DB
|
|
|
+ schedules = Schedule.select().order_by(Schedule.interval.cast("INTEGER")).execute()
|
|
|
+
|
|
|
# add schedule
|
|
|
if request.method == 'POST':
|
|
|
# Assign form values to variables
|
|
|
interval = request.form.get('interval')
|
|
|
|
|
|
+ # Check if interval is a number
|
|
|
+ try:
|
|
|
+ int(interval)
|
|
|
+ except Exception as e:
|
|
|
+ print(e)
|
|
|
+ message = 'Error creating schedule. Value submitted must be a whole number.'
|
|
|
+ return render_template('schedule.html', context={'message': message, 'schedules': schedules})
|
|
|
+
|
|
|
# Check if the schedule already exists
|
|
|
try:
|
|
|
schedule = Schedule.get(Schedule.interval == interval)
|
|
@@ -620,8 +800,7 @@ def schedule():
|
|
|
Log.create(username=session['username'], action='Created schedule for %s day interval.' % interval).save()
|
|
|
message = 'Schedule: %s created successfully' % interval
|
|
|
|
|
|
- # Get all schedules from the DB
|
|
|
- schedules = Schedule.select().order_by(Schedule.interval.cast("INTEGER")).execute()
|
|
|
+
|
|
|
|
|
|
context = {
|
|
|
'schedules': schedules,
|
|
@@ -631,6 +810,38 @@ def schedule():
|
|
|
return render_template('schedule.html', context=context)
|
|
|
|
|
|
|
|
|
+@app.route('/admin/schedule/emails')
|
|
|
+def scheduled_emails():
|
|
|
+ # Check to see if user is logged in
|
|
|
+ if not requires_auth():
|
|
|
+ return redirect(url_for('login'))
|
|
|
+
|
|
|
+ # Get all future emailreminderlogs
|
|
|
+ reminders = EmailReminderLog.select().where(EmailReminderLog.date > datetime.datetime.now()).order_by(
|
|
|
+ -EmailReminderLog.date).execute()
|
|
|
+
|
|
|
+ context = {
|
|
|
+ 'reminders': reminders,
|
|
|
+ }
|
|
|
+
|
|
|
+ return render_template('scheduled_emails.html', context=context)
|
|
|
+
|
|
|
+
|
|
|
+# remove schedule
|
|
|
+@app.route('/admin/schedule/remove/reminder/<int:id>')
|
|
|
+def reminder_remove(id):
|
|
|
+ # Check to see if user is logged in
|
|
|
+ if not requires_auth():
|
|
|
+ return redirect(url_for('login'))
|
|
|
+
|
|
|
+ # Get EmailReminderLogs with the interval of the schedule being removed
|
|
|
+ email_reminder_log = EmailReminderLog.get(EmailReminderLog.id == id)
|
|
|
+ Log.create(username=session['username'],
|
|
|
+ action='Removed %s day reminder for %s' % (email_reminder_log.interval, email_reminder_log.email)).save()
|
|
|
+ email_reminder_log.delete_instance()
|
|
|
+ return redirect(url_for('scheduled_emails'))
|
|
|
+
|
|
|
+
|
|
|
# remove schedule
|
|
|
@app.route('/admin/schedule/remove/<int:id>', methods=['GET', 'POST'])
|
|
|
def schedule_remove(id):
|
|
@@ -638,9 +849,15 @@ def schedule_remove(id):
|
|
|
if not requires_auth():
|
|
|
return redirect(url_for('login'))
|
|
|
|
|
|
+ # Get EmailReminderLogs with the interval of the schedule being removed
|
|
|
+ email_reminder_logs = EmailReminderLog.select().where(EmailReminderLog.interval == id).execute()
|
|
|
+ for email_reminder_log in email_reminder_logs:
|
|
|
+ email_reminder_log.delete_instance()
|
|
|
+
|
|
|
# Get the schedule from the DB
|
|
|
schedule = Schedule.get(Schedule.id == id)
|
|
|
schedule.delete_instance()
|
|
|
+
|
|
|
Log.create(username=session['username'], action='Removed schedule for a %s day reminder.' % schedule.interval).save()
|
|
|
return redirect(url_for('schedule'))
|
|
|
|
|
@@ -724,8 +941,9 @@ def login():
|
|
|
return render_template('login.html', context=context)
|
|
|
|
|
|
|
|
|
-# on exit of the program make sure the http server is stopped
|
|
|
-# @app.teardown_appcontext
|
|
|
+def clean_up():
|
|
|
+ shutdown_scheduler.set()
|
|
|
+ http_server.stop()
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
@@ -748,3 +966,4 @@ if __name__ == "__main__":
|
|
|
print("To stop the application close this window.")
|
|
|
print("---------------------------------------------------------------")
|
|
|
http_server.serve_forever()
|
|
|
+ atexit.register(clean_up)
|