this tutorial is from here: https://www.geeksforgeeks.org/flask-role-based-access-control/
- currently working on--> add a notary logbook feature with an editable table for Traditional Notary and Electronic Notary users.
- COMPLETE-->This will require updating the
User
model for registered users who are either Traditional Notaries or Electronic Notaries to include their commission details:commission_holder_name
,commission_number_uid
,commissioned_county
,commission_type_traditional_or_electronic
,term_issue_date
andterm_expiration_date
. - COMPLETE-->UPDATE PSQL
User
MODEL: once a user registers as a notary and successfully authenticates using NYSDOS notary credentials those credentials need to be added to the user's profile in the database. - Add editable_logbook.py and editable_logbook.html to the front end.
- create notary_logbook models for database and join it with the
User
model. - Add CRUD operations for logbook
- Add logic that checks if a user has already registered with the notary credentials.
- COMPLETE-->This will require updating the
- Add "Forgot Username" and "Forgot Password" feature using flask-security & flask-mailman
- There is a virtual environment already set up for this project so you need to activate it:
source venv/bin/activate
- In Flask, the
FLASK_APP
environment variable is used to specify how to load your application. It can be set to a python file, a module, or a package. In this case, it's being set to theapp.py
file, which is presumably where your Flask application is defined.
When you run flask run
, Flask uses the value of FLASK_APP
to find your application and run it. If FLASK_APP
is not set, Flask won't know where your application is and won't be able to run it. So before you run the application you must run the command:
export FLASK_APP=app.py
- Now you can run the application:
flask run --debugger
- Since this is a Unix-based system you must activate the PostgreSQL service with the command:
NB--> be prepared to enter your
psql
password
sudo service postgresql start
- To log in to your PSQL account you must use the
psql
command with the-U
option followed by your username:
psql -U your_username
- Helpful "general"
psql
commands:
\l
: list all your databases.\c nysdos_notaries_test
: Connect to a specific database.\dt
: List all tables in the current database.\d
: table_name: Show the structure of a specific table.\du
: List all users.\h
: Get a help on syntax of SQL commands.\?
: Lists all psql slash commands.\q
: Quit psql.
- Helpful
psql
commands to run once you are connected to a database:
\dt
: List all tables in the current database.\d table_name
: Show the structure of a specific table. Replace table_name with the name of the table you want to inspect.\dn
: List all schemas in the current database.\df
: List all functions in the current database.dv
: List all views in the current database.\x
: Toggle expanded display. This can make the output of some commands easier to read.\a
: Toggle between unaligned and aligned output mode.\timing
: Toggle timing of commands. When on, psql will show how long each command takes to execute.\i filename
: Execute commands from a file. Replace filename with the name of the file you want to execute commands from.\o filename
: Send all query results to a file. Replace filename with the name of the file you want to send results to.\q
: Quit psql.
- change roles from Admin, Teacher, Staff and Student to Admin, Principal, Traditional Notary and Electronic Notary
- files to be refactored:
app.py
- copy
app.py
and save it asapp_copy.py
in case you want to start the refactor over again. - you will have to add logic to the
def signin()
route where if the user picks "traditioanal notary" or "electronic notary" then a second signin screen shows up that asks for their commission details: full name, commission id, commissioned county and commission start and expiration dates. - you don't have to change the
roles_users
table,User Class
orRole Class
. - you don't have to change the
def signin()
route.
- copy
index.html
- save original file as "index_notes.html" in case you need to refer to it later.
- replace old roles with new roles and new routes.
signup.html
- replace old roles with new roles. The
value
matches up to the ids as defined in thecreate_roles.py
file (I think).
- replace old roles with new roles. The
mydetails.html
- change to
myaccount.html
and this page will essentially be an index of all the account subcategories that will vary depending on the user's role. For now implement the following subcategories:- admins, principals, trad notaries and e-notaries:
contactinfo.html
: first name, last name, email, phone numbercontactaddress.html
: street address line 1, street address line 2, City, State, Zip codebilling.html
: embed a Stripe API component here which should include first name on cc, last name on cc, cc number, expiration, cvv, billing address line 1, billing address line 2, billing address City, billing address State, billing address Zip code.scheduling.html
: embed a booking component here that uses a calendar and has bookling logic. Maybe include a preferred contact method (email vs phone) and a public notes section.mynotaries.html
: redirect to the /Explore route that has the table and CRUD operations or maybe embed the table with CRUD operations into this page.mydocuments.html
: embed a table with file upload capability. this will require setting up firebase document account to the user.e-signature.html
: embed the docusign test feature here,backgroundinfo.html
: this will also use Firebase to store the avatar image. Other info can be stored here such as teleconference options, languages spoken, specializations, certifications, current employer & title, etc. This may be a good placehold for if a user wants to incorporate their LinkedIn information.
- admins, principals, trad notaries and e-notaries:
- change to
signin.html
- unchanged
staff.html
,students.html
,teachers.html
,teacherslounge.html
- depracated; no longer need but keep as a reference
create_roles.py
- you will have to refactor this by changing the role names and you may want to change the ids from integers to text but this may have a negligible impact on the final code post refactor.
enotary user test #1: [email protected] abc123
enotary user test #2: [email protected] abc123
enotary user test #3: [email protected] abc123
principal user test#1 [email protected] abc123
principal user test#2-post notaryauth route refactor on 5-15-24: [email protected] abc123
enotary user test#1-post notaryauth route refactor on 5-15-24: [email protected] abc123 [created with real notary credentials below]
enotary user test#2-post notaryauth route refactor on 5-15-24: [email protected] abc123 [created with real notary credentials below]
trad_notary user test#1-post notaryauth route refactor on 5-15-24v2(second session):
[email protected] asdgfadfhassdfgadfh CHAK C CHONG 02CH5009104 Putnam Traditional 03/08/2023 03/08/2027
trad_notary user test#2-post notaryauth route refactor on 5-15-24v2(second session): --> fringe case where the commission id only has 10 characters but is 11 charaters long [email protected] abc123 KAREN M D'ANGELO 01 D6390284 Monroe Traditional 04/15/2023 04/15/2027
trad_notary user test#3-post notaryauth route refactor on 5-15-24v3(second session): --> fringe case where the commission id only has 10 characters but is 11 charaters long [email protected] abc123 GRACE PRUNO 01 P4603811 Suffolk Traditional 01/31/2023 01/31/2027
https://data.ny.gov/resource/rwbv-mz6z.json?commission_number_uid=01 P4603811
trad_notary user test#4-post notaryauth route refactor on 5-15-24v3(second session): --> not a fringe case [email protected] abc123 RASHAD SHARIF 01SH6313188 Rockland Traditional 01/20/2023 01/20/2027
if you go to the data.ny.gov website and search for records that contain a sapce " " using the 'contains' query term you get 236 records that are 11 characters long but only contain 10 characters and a space in the middle of the textstring. Querying these records is problematic from the API perspective and even copying and pasting the commission holder's name in the URL doesn't always work.
e.g. these five GET requests return an empty [ ]
https://data.ny.gov/resource/rwbv-mz6z.json?commission_holder_name=GRACE PRUNO
https://data.ny.gov/resource/rwbv-mz6z.json?commission_holder_name=JOHN A OWENS JR
https://data.ny.gov/resource/rwbv-mz6z.json?commission_holder_name=CAROL A BLAKE
https://data.ny.gov/resource/rwbv-mz6z.json?commission_holder_name=KAREN M D'ANGELO
https://data.ny.gov/resource/rwbv-mz6z.json?commission_holder_name=MICHELE A GREEN HOSANG
while https://data.ny.gov/resource/rwbv-mz6z.json?commission_holder_name=COSTA-TRAHAN CYNTHIA A
returns:
[
{
"commission_holder_name": "COSTA-TRAHAN CYNTHIA A",
"commission_number_uid": "02A 5074644",
"business_address_1_if_available": "COSTA & ASSOCIATES",
"business_address_2_if_available": "56 N. BROADWAY",
"business_city_if_available": "NYACK",
"business_state_if_available": "NY",
"business_zip_if_available": "10960",
"commissioned_county": "Rockland",
"commission_type_traditional_or_electronic": "Traditional",
"term_issue_date": "2023-05-17T00:00:00.000",
"term_expiration_date": "2027-05-17T00:00:00.000",
"georeference": {
"type": "Point",
"coordinates": [
-73.9249517,
41.0917555
]
}
}
]
use this fake default to log in as "a Principal user" NB--> 5/17/24 registration works fine
[email protected] abc123
[email protected] abc123
[email protected] abc123
ARTHUR John HAUSER 01HA0018990 Kings Electronic 12/21/2023 12/21/2027
Debugging notes from issue with Flask
session
and Flask_Security
's SQLAlchemySessionUserDatastore
and SQLAlchemyUserDatastore
-
Flask's
session
object: This is a way to store information specific to a user from one request to the next. It's a dictionary-like object that uses a signed cookie to keep the session secure. In your code, you're using the session to store the email, password, and role_id of a user during the signup process. This data is then available to you in subsequent requests, even if they happen in a different route, like/notaryauth
. -
SQLAlchemySessionUserDatastore
andSQLAlchemyUserDatastore
: These are classes provided by Flask-Security that allow you to interact with your User and Role models. They provide methods for creating users, finding users, adding roles to users, etc. In your code, you're usingSQLAlchemySessionUserDatastore
to create an instance ofuser_datastore
at the start of your application. Thisuser_datastore
is then used in your/signup
and/notaryauth
routes to create users and add roles to them.
The SQLAlchemySessionUserDatastore
is designed to work with SQLAlchemy's session-based transactions, which means it doesn't commit changes to the database immediately. Instead, it waits until you call db.session.commit()
. This can be useful if you want to make multiple changes and then commit them all at once.
On the other hand, SQLAlchemyUserDatastore
commits changes immediately. In your /notaryauth
route, you're creating a new instance of SQLAlchemyUserDatastore
and using it to create a user and add a role to them. These changes are committed to the database as soon as you call the methods.
In terms of how these are impacting your code, the Flask session
is allowing you to store user data across requests, while the SQLAlchemyUserDatastore
is letting you interact with your User and Role models. However, it's unusual to use both SQLAlchemySessionUserDatastore
and SQLAlchemyUserDatastore
in the same application. Typically, you would choose one or the other based on whether you want to use session-based transactions or not.
user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
This code creates an instance of the SQLAlchemySessionUserDatastore
class, which is a part of the Flask-Security
extension. This class is a datastore adapter that allows Flask-Security
to interact with your User
and Role
models using SQLAlchemy
's session-based transactions.
Here's a breakdown of the parameters:
-
db.session
: This is theSQLAlchemy
session
that will be used to interact with the database.SQLAlchemy
session
s manage all the interactions with the database and keep track of all the objects you've loaded or associated with it. They also begin and control transactions. -
User
: This is yourUser
model class. It should be aSQLAlchemy
model that includes certain fields required byFlask-Security
, such as a password and email. -
Role
: This is yourRole
model class. It should be aSQLAlchemy
model that represents roles that users can have.
The SQLAlchemySessionUserDatastore
instance (user_datastore
) can then be used to create users, roles, and to add roles to users, among other things. It's designed to work with SQLAlchemy's session-based transactions, which means it doesn't commit changes to the database immediately. Instead, it waits until you call db.session.commit()
. This can be useful if you want to make multiple changes and then commit them all at once.
security = Security(app, user_datastore)
This code initializes the Flask-Security
extension with your Flask
application (app
) and the SQLAlchemy
session-based datastore (user_datastore
).
Here's a breakdown of the parameters:
-
app
: This is yourFlask
application instance.Flask-Security
needs this to set up routes, templates, and other configuration options. -
user_datastore
: This is an instance ofSQLAlchemySessionUserDatastore
that you've created. It's used byFlask-Security
to interact with yourUser
andRole
models.
The Security
class is a part of the Flask-Security
extension. It provides a number of features for security-related aspects of your application, such as user authentication, role management, password hashing, and session management.
By creating an instance of the Security
class and passing in your Flask
application and datastore, you're setting up Flask-Security
to handle these aspects of your application. This includes setting up routes for user registration, login, logout, and more. It also includes setting up the necessary views and templates for these routes.
When you initialize Flask-Security
by creating an instance of the Security class, you're essentially telling Flask-Security
to set up a number of routes, views, and templates that are commonly used in web applications for user management. These include routes for user registration, login, logout, and more.
For example, Flask-Security
will automatically create the following routes:
-
/login
: This route is used for user login. It will display a login form to the user and handle the submission of this form to authenticate the user. -
/logout
: This route is used to log out the user. It will clear the user's session and redirect them to the login page. -
/register
: This route is used for user registration. It will display a registration form to the user and handle the submission of this form to create a new user. -
/reset
: This route is used to reset a user's password. It will display a form to the user where they can enter their email address.Flask-Security
will then send them an email with a link to reset their password.
In addition to these routes, Flask-Security
will also set up the necessary views and templates. The views are the functions that handle the requests to these routes, and the templates are the HTML files that define what the user sees when they visit these routes.
For example, for the /login
route, Flask-Security
will create a view function that handles the GET and POST requests to this route. It will also create a login.html
template that defines the login form that the user sees.
All of this setup is done automatically when you create an instance of the Security
class and pass in your Flask application and datastore. This saves you the time and effort of having to create these routes, views, and templates yourself.
old /notaryauth
route code that was interfering with new code while refactoring to use flask-wtforms
@app.route("/signup", methods=["GET", "POST"])
def signup():
msg = ""
if request.method == "POST":
user = User.query.filter_by(email=request.form["email"]).first()
if user:
msg = "User already exist"
return render_template("signup.html", msg=msg)
role = Role.query.filter_by(id=request.form["options"]).first()
user = user_datastore.create_user(
email=request.form["email"], password=request.form["password"]
)
user_datastore.add_role_to_user(user, role)
db.session.commit()
login_user(user)
# Store email and password in session
session["email"] = request.form["email"]
session["password"] = request.form["password"]
session["role_id"] = role.id
if role.name in ["Traditional Notary", "Electronic Notary"]:
session["form_data"] = request.form.to_dict()
return redirect(url_for("signupnotary")) # Redirect to signupnotary
else:
return redirect(url_for("index"))
else:
return render_template("signup.html", msg=msg)
@app.route("/signupnotary", methods=["GET"])
def signupnotary():
# Render the signupnotary page
return render_template("signupnotary.html")
@app.route("/notaryauth", methods=["GET", "POST"])
def notaryauth():
# Retrieve form data from session
form_data = session.get("form_data", {})
email = form_data.get("email")
password = form_data.get("password")
full_name = form_data.get("full_name")
commission_id = form_data.get("commission_id")
commissioned_county = form_data.get("commissioned_county")
commission_start_date_str = form_data.get("commission_start_date")
if commission_start_date_str is not None:
commission_start_date = datetime.strptime(commission_start_date_str, "%Y-%m-%d")
else:
commission_start_date = None
commission_expiration_date_str = form_data.get("commission_expiration_date")
if commission_expiration_date_str is not None:
commission_expiration_date = datetime.strptime(
commission_expiration_date_str, "%Y-%m-%d"
)
else:
commission_expiration_date = None
role_mapping = {
3: "Traditional",
4: "Electronic",
}
role_value = int(session.get("role_id", 0))
commission_type = role_mapping.get(role_value)
if commission_id is not None:
commission_id_encoded = urllib.parse.quote_plus(str(commission_id))
else:
commission_id_encoded = None
response = requests.get(
"https://data.ny.gov/resource/rwbv-mz6z.json",
params={
"commission_holder_name": full_name,
"commission_number_uid": commission_id_encoded,
"commissioned_county": commissioned_county,
"commission_type_traditional_or_electronic": commission_type,
"term_issue_date": commission_start_date_str,
"term_expiration_date": commission_expiration_date_str,
},
)
data = response.json()
print("Data from API: ", data)
if not data or not isinstance(data, list) or len(data) == 0:
return (
jsonify({"error": "No matching data found in the API's database"}),
400,
)
print(
"Data to map to: ",
full_name,
commissioned_county,
commission_start_date,
commission_expiration_date,
)
if (
data[0]["commission_holder_name"].lower() == full_name.lower()
and data[0]["commissioned_county"].lower() == commissioned_county.lower()
and datetime.strptime(data[0]["term_issue_date"], "%Y-%m-%dT%H:%M:%S.%f")
== commission_start_date
and datetime.strptime(data[0]["term_expiration_date"], "%Y-%m-%dT%H:%M:%S.%f")
== commission_expiration_date
):
user = user_datastore.create_user(email=email, password=password)
role = user_datastore.find_role(
"Traditional Notary"
if data[0]["commission_type_traditional_or_electronic"] == "Traditional"
else "Electronic Notary"
)
user_datastore.add_role_to_user(user, role)
notary_credentials = NotaryCredentials(
user_id=user.id,
commission_holder_name=full_name,
commission_number_uid=commission_id,
commissioned_county=commissioned_county,
commission_type_traditional_or_electronic=commission_type,
term_issue_date=commission_start_date,
term_expiration_date=commission_expiration_date,
)
db.session.add(notary_credentials)
db.session.commit()
login_user(user)
return redirect(url_for("index"))
else:
return render_template(
"signupnotary.html",
error="The provided data does not match the API's database",
)
https://jsdoc.app/tags-example https://docs.oracle.com/javase/8/docs/api/ https://docs.python.org/3/library/pydoc.html https://medium.com/@peterkong/comparison-of-python-documentation-generators-660203ca3804
use this documentation guide to create basic documentation for the project. https://www.sphinx-doc.org/en/master/usage/quickstart.html