make all this have a login page, use ldap, put a logo on, moved some upstream stuff to static/ -- need to do more here to be consistent with bootstrap 5, but for another day
This commit is contained in:
121
main.py
121
main.py
@@ -1,11 +1,16 @@
|
||||
from flask import Flask, render_template, request, redirect, jsonify
|
||||
from flask import Flask, render_template, request, redirect, jsonify, url_for
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from flask_marshmallow import Marshmallow
|
||||
from flask_bootstrap import Bootstrap
|
||||
from wtforms import SubmitField, StringField, HiddenField, SelectField, IntegerField, TextAreaField, validators
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_compress import Compress
|
||||
from status import st, Status
|
||||
# for ldap auth
|
||||
from flask_ldap3_login import LDAP3LoginManager
|
||||
from flask_login import LoginManager, login_user, login_required, UserMixin, current_user, logout_user
|
||||
from flask_ldap3_login.forms import LDAPLoginForm
|
||||
import re
|
||||
import os
|
||||
|
||||
@@ -21,10 +26,30 @@ else:
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config.from_mapping( SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G')
|
||||
|
||||
# ldap config vars: (the last one is required, or python ldap freaks out)
|
||||
app.config['LDAP_HOST'] = 'mara.ddp.net'
|
||||
app.config['LDAP_BASE_DN'] = 'dc=depaoli,dc=id,dc=au'
|
||||
app.config['LDAP_USER_DN'] = 'ou=users'
|
||||
app.config['LDAP_GROUP_DN'] = 'ou=groups'
|
||||
app.config['LDAP_USER_RDN_ATTR'] = 'cn'
|
||||
app.config['LDAP_USER_LOGIN_ATTR'] = 'uid'
|
||||
app.config['LDAP_BIND_USER_DN'] = None
|
||||
app.config['LDAP_BIND_USER_PASSWORD'] = None
|
||||
app.config['LDAP_GROUP_OBJECT_FILTER'] = '(objectclass=posixGroup)'
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
ma = Marshmallow(app)
|
||||
Bootstrap(app)
|
||||
|
||||
# setup ldap for auth
|
||||
login_manager = LoginManager(app) # Setup a Flask-Login Manager
|
||||
ldap_manager = LDAP3LoginManager(app) # Setup a LDAP3 Login Manager.
|
||||
login_manager.login_view = "login" # default login route, failed with url_for, so hard-coded
|
||||
|
||||
# enable compression for http / speed
|
||||
Compress(app)
|
||||
|
||||
################################# Now, import non-book classes ###################################
|
||||
from author import Author, AuthorForm, AuthorSchema, GetAuthors
|
||||
from publisher import Publisher, PublisherForm, PublisherSchema, GetPublisherById
|
||||
@@ -35,6 +60,7 @@ from owned import Owned, OwnedForm, OwnedSchema, GetOwnedById
|
||||
from rating import Rating, RatingForm, RatingSchema, GetRatingById
|
||||
from loan import Loan, LoanForm, LoanSchema
|
||||
from series import Series, SeriesForm, SeriesSchema, ListOfSeriesWithMissingBooks, CalcAvgRating
|
||||
from user import BDBUser
|
||||
|
||||
####################################### CLASSES / DB model #######################################
|
||||
class QuickParentBook:
|
||||
@@ -246,12 +272,67 @@ app.jinja_env.globals['GetRatingById'] = GetRatingById
|
||||
app.jinja_env.globals['SeriesBookNum'] = SeriesBookNum
|
||||
app.jinja_env.globals['ClearStatus'] = st.ClearStatus
|
||||
app.jinja_env.globals['ListOfSeriesWithMissingBooks'] = ListOfSeriesWithMissingBooks
|
||||
app.jinja_env.globals['current_user'] = current_user
|
||||
|
||||
book_schema = BookSchema()
|
||||
books_schema = BookSchema(many=True)
|
||||
|
||||
# Declare a User Loader for Flask-Login.
|
||||
# Returns the User if it exists in our 'database', otherwise returns None.
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
bdbu=BDBUser.query.filter(BDBUser.dn==id).first()
|
||||
return bdbu
|
||||
|
||||
# Declare The User Saver for Flask-Ldap3-Login
|
||||
# This method is called whenever a LDAPLoginForm() successfully validates.
|
||||
# store the user details / session in the DB if it is not in there already
|
||||
@ldap_manager.save_user
|
||||
def save_user(dn, username, data, memberships):
|
||||
bdbu=BDBUser.query.filter(BDBUser.dn==dn).first()
|
||||
# if we already have a valid user/session, and say the web has restarted, just re-use it, dont make more users
|
||||
if bdbu:
|
||||
return bdbu
|
||||
bdbu=BDBUser(dn=dn)
|
||||
db.session.add(bdbu)
|
||||
db.session.commit()
|
||||
return bdbu
|
||||
|
||||
# POST is when user submits pwd & uses flask-login to hit ldap, validate pwd
|
||||
# if valid, then we save user/session into the DB via login_user() -> calls save_user()
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
# Instantiate a LDAPLoginForm which has a validator to check if the user
|
||||
# exists in LDAP.
|
||||
form = LDAPLoginForm()
|
||||
form.submit.label.text="Login"
|
||||
|
||||
# the re matches on any special LDAP chars, we dont want someone
|
||||
# ldap-injecting our username, so send them back to the login page instead
|
||||
if request.method == 'POST' and re.search( r'[()\\*&!]', request.form['username']):
|
||||
print( f"WARNING: Detected special LDAP chars in username: {request.form['username']}")
|
||||
return redirect(url_for('login'))
|
||||
if form.validate_on_submit():
|
||||
# Successfully logged in, We can now access the saved user object via form.user.
|
||||
login_user(form.user, remember=True) # Tell flask-login to log them in.
|
||||
next = request.args.get("next")
|
||||
if next:
|
||||
return redirect(next) # Send them back where they came from
|
||||
else:
|
||||
return redirect( url_for('main_page') )
|
||||
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect('/login')
|
||||
|
||||
|
||||
####################################### ROUTES #######################################
|
||||
@app.route("/search", methods=["POST"])
|
||||
@login_required
|
||||
def search():
|
||||
if 'InDBox' in request.form:
|
||||
# removes already loaned books from list of books to loan out
|
||||
@@ -264,12 +345,14 @@ def search():
|
||||
return render_template("books.html", books=books, InDBox=InDBox)
|
||||
|
||||
@app.route("/books", methods=["GET"])
|
||||
@login_required
|
||||
def books():
|
||||
books = Book.query.all()
|
||||
AddSubs(books)
|
||||
return render_template("books.html", books=books )
|
||||
|
||||
@app.route("/books_for_loan/<id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def books_for_loan(id):
|
||||
books = Book.query.join(Book_Loan_Link).filter(Book_Loan_Link.loan_id==id).order_by(Book.id).all()
|
||||
if request.method == 'POST':
|
||||
@@ -332,6 +415,7 @@ def CalcMoveForBookInSeries( id, bid, dir ):
|
||||
book2.MoveBookInSeries( id, b2_move_by )
|
||||
|
||||
@app.route("/books_for_series/<id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def books_for_series(id):
|
||||
if request.method == 'POST':
|
||||
if 'move_button' in request.form:
|
||||
@@ -346,6 +430,7 @@ def books_for_series(id):
|
||||
return render_template("books_for_series.html", books=books, series=series)
|
||||
|
||||
@app.route("/subbooks_for_book/<id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def subbooks_for_book(id):
|
||||
if request.method == 'POST':
|
||||
if 'move_button' in request.form:
|
||||
@@ -389,6 +474,7 @@ def subbooks_for_book(id):
|
||||
# /books (or /book/<parent_id> -- if you added a sub-book of parent_id
|
||||
################################################################################
|
||||
@app.route("/remove_subbook", methods=["POST"])
|
||||
@login_required
|
||||
def remove_sub_book():
|
||||
try:
|
||||
db.engine.execute("delete from book_sub_book_link where book_id = {} and sub_book_id = {}".format( request.form['rem_sub_parent_id'], request.form['rem_sub_sub_book_id']) )
|
||||
@@ -404,6 +490,7 @@ def remove_sub_book():
|
||||
# /books (or /book/<parent_id> -- if you added a sub-book of parent_id
|
||||
################################################################################
|
||||
@app.route("/book", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def new_book():
|
||||
form = BookForm(request.form)
|
||||
page_title='Create new Book'
|
||||
@@ -475,6 +562,7 @@ def new_book():
|
||||
|
||||
|
||||
@app.route("/book/<id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def book(id):
|
||||
book_form = BookForm(request.form)
|
||||
page_title='Edit Book'
|
||||
@@ -619,6 +707,7 @@ def GetCount( what, where ):
|
||||
return rtn
|
||||
|
||||
@app.route("/stats", methods=["GET"])
|
||||
@login_required
|
||||
def stats():
|
||||
stats=[]
|
||||
|
||||
@@ -637,6 +726,7 @@ def stats():
|
||||
return render_template("stats.html", stats=stats )
|
||||
|
||||
@app.route("/rem_books_from_loan/<id>", methods=["POST"])
|
||||
@login_required
|
||||
def rem_books_from_loan(id):
|
||||
for field in request.form:
|
||||
rem_id=int(re.findall( '\d+', field )[0])
|
||||
@@ -649,6 +739,7 @@ def rem_books_from_loan(id):
|
||||
return jsonify(success=True)
|
||||
|
||||
@app.route("/add_books_to_loan/<id>", methods=["POST"])
|
||||
@login_required
|
||||
def add_books_to_loan(id):
|
||||
for field in request.form:
|
||||
add_id=int(re.findall( '\d+', field )[0])
|
||||
@@ -662,6 +753,7 @@ def add_books_to_loan(id):
|
||||
return jsonify(success=True)
|
||||
|
||||
@app.route("/rem_parent_books_from_series/<pid>", methods=["POST"])
|
||||
@login_required
|
||||
def rem_parent_books_from_series(pid):
|
||||
print ("pid={}".format(pid) )
|
||||
try:
|
||||
@@ -674,12 +766,14 @@ def rem_parent_books_from_series(pid):
|
||||
return jsonify(success=True)
|
||||
|
||||
@app.route("/books_on_shelf", methods=["GET"])
|
||||
@login_required
|
||||
def books_on_shelf():
|
||||
books = Book.query.join(Owned).filter(Owned.name=='Currently Owned').all()
|
||||
RemSubs(books)
|
||||
return render_template("books.html", books=books, page_title="Books on Shelf", order_by="Author(s)", show_cols='', hide_cols='' )
|
||||
|
||||
@app.route("/unrated_books", methods=["GET"])
|
||||
@login_required
|
||||
def unrated_books():
|
||||
books = Book.query.join(Condition,Owned).filter(Rating.name=='Undefined',Owned.name=='Currently Owned').all()
|
||||
return render_template("books.html", books=books, page_title="Books with no rating", show_cols='Rating', hide_cols='' )
|
||||
@@ -710,39 +804,50 @@ def FindMissingBooks():
|
||||
return books
|
||||
|
||||
@app.route("/missing_books", methods=["GET"])
|
||||
@login_required
|
||||
def missing_books():
|
||||
books=FindMissingBooks()
|
||||
return render_template("missing.html", books=books )
|
||||
|
||||
@app.route("/wishlist", methods=["GET"])
|
||||
@login_required
|
||||
def wishlist():
|
||||
books = Book.query.join(Owned).filter(Owned.name=='On Wish List').all()
|
||||
return render_template("books.html", books=books, page_title="Books On Wish List", show_cols='', hide_cols='Publisher,Condition,Covertype' )
|
||||
|
||||
@app.route("/books_to_buy", methods=["GET"])
|
||||
@login_required
|
||||
def books_to_buy():
|
||||
books = Book.query.join(Owned).filter(Owned.name=='On Wish List').all()
|
||||
missing = FindMissingBooks()
|
||||
return render_template("to_buy.html", books=books, missing=missing, page_title="Books To Buy")
|
||||
|
||||
@app.route("/needs_replacing", methods=["GET"])
|
||||
@login_required
|
||||
def needs_replacing():
|
||||
books = Book.query.join(Condition,Owned).filter(Condition.name=='Needs Replacing',Owned.name=='Currently Owned').all()
|
||||
return render_template("books.html", books=books, page_title="Books that Need Replacing", show_cols='', hide_cols='' )
|
||||
|
||||
@app.route("/sold", methods=["GET"])
|
||||
@login_required
|
||||
def sold_books():
|
||||
books = Book.query.join(Owned).filter(Owned.name=='Sold').all()
|
||||
return render_template("books.html", books=books, page_title="Books that were Sold", show_cols='', hide_cols='' )
|
||||
|
||||
@app.route("/poor_rating", methods=["GET"])
|
||||
@login_required
|
||||
def poor_rating_books():
|
||||
books = Book.query.join(Rating,Owned).filter(Rating.id>6,Rating.name!='Undefined',Owned.name=='Currently Owned').all()
|
||||
return render_template("books.html", books=books, page_title="Books that have a Poor Rating (<5 out of 10)", show_cols='Rating', hide_cols='' )
|
||||
|
||||
|
||||
# default page, just the navbar
|
||||
@app.route("/", methods=["GET"])
|
||||
@login_required
|
||||
def main_page():
|
||||
# Redirect users who are not logged in.
|
||||
if not current_user or current_user.is_anonymous:
|
||||
return redirect(url_for('login'))
|
||||
return render_template("base.html", alert=st.GetAlert(), message=st.GetMessage())
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -750,3 +855,17 @@ if __name__ == "__main__":
|
||||
app.run(ssl_context=('/etc/letsencrypt/live/book.depaoli.id.au/cert.pem', '/etc/letsencrypt/live/book.depaoli.id.au/privkey.pem'), host="0.0.0.0", debug=False)
|
||||
else:
|
||||
app.run(host="0.0.0.0", debug=True)
|
||||
|
||||
###############################################################################
|
||||
# This func creates a new filter in jinja2 to test to hand back the username
|
||||
# from the ldap dn
|
||||
################################################################################
|
||||
@app.template_filter('Username')
|
||||
def _jinja2_filter_parentpath(dn):
|
||||
# pull apart a dn (uid=xxx,dn=yyy,etc), and return the xxx
|
||||
username=str(dn)
|
||||
s=username.index('=')
|
||||
s+=1
|
||||
f=username.index(',')
|
||||
return username[s:f]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user