Skip to content

Commit ae3b827

Browse files
committed
Fix various application issues, especially session storage
1 parent 15cdde8 commit ae3b827

16 files changed

+113
-67
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ quart-flask-patch = "*"
2323
aioquic = "*"
2424
psycopg-pool = "*"
2525
psycopg = {extras = ["binary"], version = "*"}
26+
jsonpickle = "*"
2627

2728
[dev-packages]
2829
pytest-flask = "*"

Pipfile.lock

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ $ pipenv install --python=/path/to/python
4646

4747
## Database Migration
4848

49+
- Copy `env.py` to `migrations/` folder.
50+
- Set the values -f `DB_foo` in `/etc/pythonrestapi_config.json`
4951
- run migrations initialization with db init command:
5052

5153
```

env.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
config.set_section_option(section, "DB_PASSWORD", urllib.parse.quote_plus(json_config['DB_PASSWORD']).replace("%", "%%"))
3636
config.set_section_option(section, "DB_HOST", json_config["DB_HOST"])
3737
config.set_section_option(section, "DB_DATABASE", json_config["DB_DATABASE"])
38-
def run_migrations_offline():
38+
def run_migrations_offline() -> None:
3939
"""Run migrations in 'offline' mode.
4040
4141
This configures the context with just a URL
@@ -59,15 +59,15 @@ def run_migrations_offline():
5959
context.run_migrations()
6060

6161

62-
def run_migrations_online():
62+
def run_migrations_online() -> None:
6363
"""Run migrations in 'online' mode.
6464
6565
In this scenario we need to create an Engine
6666
and associate a connection with the context.
6767
6868
"""
6969
connectable = engine_from_config(
70-
config.get_section(config.config_ini_section),
70+
config.get_section(config.config_ini_section, {}),
7171
prefix="sqlalchemy.",
7272
poolclass=pool.NullPool,
7373
)

migrations/env.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
config.set_section_option(section, "DB_PASSWORD", urllib.parse.quote_plus(json_config['DB_PASSWORD']).replace("%", "%%"))
3636
config.set_section_option(section, "DB_HOST", json_config["DB_HOST"])
3737
config.set_section_option(section, "DB_DATABASE", json_config["DB_DATABASE"])
38-
def run_migrations_offline():
38+
39+
def run_migrations_offline() -> None:
3940
"""Run migrations in 'offline' mode.
4041
4142
This configures the context with just a URL
@@ -59,15 +60,15 @@ def run_migrations_offline():
5960
context.run_migrations()
6061

6162

62-
def run_migrations_online():
63+
def run_migrations_online() -> None:
6364
"""Run migrations in 'online' mode.
6465
6566
In this scenario we need to create an Engine
6667
and associate a connection with the context.
6768
6869
"""
6970
connectable = engine_from_config(
70-
config.get_section(config.config_ini_section),
71+
config.get_section(config.config_ini_section, {}),
7172
prefix="sqlalchemy.",
7273
poolclass=pool.NullPool,
7374
)

migrations/script.py.mako

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,24 @@ Revises: ${down_revision | comma,n}
55
Create Date: ${create_date}
66

77
"""
8+
from typing import Sequence, Union
9+
810
from alembic import op
911
import sqlalchemy as sa
1012
${imports if imports else ""}
1113

1214
# revision identifiers, used by Alembic.
13-
revision = ${repr(up_revision)}
14-
down_revision = ${repr(down_revision)}
15-
branch_labels = ${repr(branch_labels)}
16-
depends_on = ${repr(depends_on)}
15+
revision: str = ${repr(up_revision)}
16+
down_revision: Union[str, None] = ${repr(down_revision)}
17+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
1719

1820

1921
def upgrade() -> None:
22+
"""Upgrade schema."""
2023
${upgrades if upgrades else "pass"}
2124

2225

2326
def downgrade() -> None:
27+
"""Downgrade schema."""
2428
${downgrades if downgrades else "pass"}

migrations/versions/a4691bb6ae7d_initial_migration.py renamed to migrations/versions/0c9d8b840acc_initial_migration.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
"""Initial migration
22
3-
Revision ID: a4691bb6ae7d
3+
Revision ID: 0c9d8b840acc
44
Revises:
5-
Create Date: 2022-10-01 15:11:29.047624
5+
Create Date: 2025-03-19 14:01:32.354283
66
77
"""
8+
from typing import Sequence, Union
9+
810
from alembic import op
911
import sqlalchemy as sa
1012

1113

1214
# revision identifiers, used by Alembic.
13-
revision = 'a4691bb6ae7d'
14-
down_revision = None
15-
branch_labels = None
16-
depends_on = None
15+
revision: str = '0c9d8b840acc'
16+
down_revision: Union[str, None] = None
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
1719

1820

1921
def upgrade() -> None:
22+
"""Upgrade schema."""
2023
# ### commands auto generated by Alembic - please adjust! ###
2124
op.create_table('authors',
2225
sa.Column('id', sa.Integer(), nullable=False),
@@ -59,6 +62,7 @@ def upgrade() -> None:
5962

6063

6164
def downgrade() -> None:
65+
"""Downgrade schema."""
6266
# ### commands auto generated by Alembic - please adjust! ###
6367
op.drop_table('books')
6468
op.drop_index(op.f('ix_users_phone'), table_name='users')

src/common/Authentication.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import jwt, logging
2-
from datetime import datetime, timezone
1+
import jwt, logging, jsonpickle
2+
from datetime import datetime, timezone, timedelta
33
from quart import json, Response, session, redirect, url_for, session, abort, current_app
44
#from flask_oidc import OpenIDConnect
55
from functools import wraps
@@ -23,7 +23,7 @@ def generate_token(user_id):
2323
now = datetime.now(timezone.utc)
2424
# https://pyjwt.readthedocs.io/en/latest/usage.html#registered-claim-names
2525
payload = {
26-
"exp": now + datetime.timedelta(hours=1),
26+
"exp": now + timedelta(hours=1),
2727
"iat": now,
2828
"nbf": now,
2929
"iss": "urn:PythonFlaskRestAPI",
@@ -73,11 +73,14 @@ def actual_auth_required(func):
7373
def decorated_auth_required(*args, **kwargs):
7474
session["url"] = url_for(url, *args, **kwargs) if url else url
7575
#if "api-token" not in request.headers:
76-
if "user" not in session or not session["user"] or not session["user"]["token"]:
76+
if "user" not in session or not session["user"]: # or not session["user"]["token"]:
7777
#await flash(f"Please login to continue.", "info")
7878
return redirect(url_for("auth.login"))
79+
user = jsonpickle.decode(session['user'])
80+
if not user or not hasattr(user, 'token'):
81+
return redirect(url_for("auth.login"))
7982
#token = request.headers.get("api-token")
80-
data = Authentication.decode_token(session["user"]["token"])
83+
data = Authentication.decode_token(user.token)
8184
if data["error"]:
8285
logging.warning(f"[Auth] error: {data['error']}!")
8386
#await flash(f"{data['error']}", "danger")
@@ -100,8 +103,12 @@ def require_role(role):
100103
def decorated_require_role(func):
101104
@wraps(func)
102105
def wrapped_require_role(*args, **kwargs):
103-
if Authentication.isAuthenticated() and role in session["user"]["roles"]:
104-
return func(*args, **kwargs)
106+
if Authentication.isAuthenticated() and "user" in session:
107+
user = jsonpickle.decode(session['user'])
108+
if role in user.roles:
109+
return func(*args, **kwargs)
110+
else:
111+
return abort(403)
105112
else:
106113
return abort(403)
107114
return wrapped_require_role

src/controllers/AuthenticationController.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import logging
1+
import logging, jsonpickle
22
from quart import request, json, Blueprint, session, render_template, session, redirect, url_for
33
from marshmallow import ValidationError
44
from datetime import datetime, timezone
@@ -40,11 +40,12 @@ async def login():
4040
logging.warning(f"[Auth] Invalid email or password {data.get('email')}!")
4141
return await render_template("login.html", title="Welcome to Python Flask RESTful API", error="Invalid email or password!")
4242
ser_data = user_schema.dump(user)
43-
token = Authentication.generate_token(ser_data.get("id"))
44-
session["user"] = {"id": ser_data.get("id"), "email": user.email, "token": token}
43+
user.token = Authentication.generate_token(ser_data.get("id"))
44+
#session["user"] = {"id": ser_data.get("id"), "email": user.email, "token": token}
4545
#return custom_response({"jwt_token": token}, 200)
4646
data["lastlogin"] = datetime.now(timezone.utc)
4747
user.update(data)
48+
session['user'] = jsonpickle.encode(user)
4849
logging.info(f"[Auth] User {user.email} logged in")
4950
if "url" in session and session["url"]:
5051
return redirect(session["url"])
@@ -105,7 +106,7 @@ async def logout():
105106
User Logout
106107
"""
107108
print(f"logout()")
108-
logging.info(f"[Auth] User {session['user']['email']} logged out")
109+
logging.info(f"[Auth] User logged out")
109110
session["url"] = None
110111
session["user"] = None
111112
return redirect(url_for("home.index"))
@@ -127,7 +128,8 @@ async def profile():
127128
"""
128129
Get my profile
129130
"""
130-
user = UserModel.get_user(session["user"]["id"])
131+
u = jsonpickle.decode(session['user'])
132+
user = UserModel.get_user(u.id)
131133
if not user:
132-
raise Exception(f"User {session['user']['id']} not found!")
134+
raise Exception(f"User {u.id} not found!")
133135
return custom_response(user_schema.dump(user), 200)

src/controllers/AuthorController.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import re, logging
1+
import re, logging, jsonpickle
22
from quart import request, Blueprint, session, render_template, flash, redirect, url_for
33
from marshmallow import ValidationError
44
from datetime import datetime, timezone
@@ -27,6 +27,7 @@ async def create():
2727
"""
2828
Create Author
2929
"""
30+
user = jsonpickle.decode(session['user'])
3031
if request.method == "POST":
3132
try:
3233
form = await request.form
@@ -65,12 +66,12 @@ async def create():
6566
author = AuthorModel(data)
6667
author.save()
6768
await flash(f"Author {author.firstname}, {author.lastname} created successfully!", "success")
68-
logging.info(f"User {session['user']['email']} created author {author.email} successfully!")
69+
logging.info(f"User {user.email} created author {author.email} successfully!")
6970
return redirect(url_for("author.index"))
7071
except ValidationError as err:
7172
errors = err.messages
7273
valid_data = err.valid_data
73-
logging.error(f"User {session['user']['email']} failed to creat author! Exception: {errors}")
74+
logging.error(f"User {user.email} failed to creat author! Exception: {errors}")
7475
await flash(f"Failed to create author! {err.messages}", "danger")
7576
return redirect(url_for("author.create"))
7677
return await render_template("author_create.html", title="Welcome to Python Flask RESTful API")
@@ -81,7 +82,7 @@ async def get_author(id):
8182
"""
8283
Get Auhor 'id'
8384
"""
84-
author = auhor_schema.dump(AuthorModel.get_author(id))
85+
author = author_schema.dump(AuthorModel.get_author(id))
8586
if not author:
8687
return custom_response({"error": f"Author {id} not found!"}, 404)
8788
return custom_response(author_schema.dump(author), 200)
@@ -125,6 +126,7 @@ async def update(id):
125126
"""
126127
Update Author 'id'
127128
"""
129+
user = jsonpickle.decode(session['user'])
128130
try:
129131
req_data = await request.get_json()
130132
data = author_schema.load(req_data, partial=True)
@@ -136,12 +138,12 @@ async def update(id):
136138
await flash(f"Trying to update non-existing author {id}!", "warning")
137139
return redirect(url_for("author.index"))
138140
author.update(data)
139-
logging.info(f"User {session['user']['email']} updated author {author.email} successfully!")
141+
logging.info(f"User {user.email} updated author {author.email} successfully!")
140142
await flash(f"Author {author.firstname}, {author.lastname} updated successfully!", "success")
141143
except ValidationError as err:
142144
errors = err.messages
143145
valid_data = err.valid_data
144-
logging.error(f"User {session['user']['email']} failed to update author {id}! Exception: {errors}")
146+
logging.error(f"User {user.email} failed to update author {id}! Exception: {errors}")
145147
await flash(f"Failed to update author {id}! Exception: {errors}", "danger")
146148
return redirect(url_for("author.index"))
147149

@@ -151,18 +153,19 @@ async def delete(id):
151153
"""
152154
Delete Author 'id'
153155
"""
156+
user = jsonpickle.decode(session['user'])
154157
try:
155158
author = AuthorModel.get_author(id)
156159
if not author:
157160
await flash(f"Trying to delete non-existing author {id}!", "warning")
158161
return redirect(url_for("author.index"))
159162
author.delete()
160-
logging.warning(f"User {session['user']['email']} deleted author {author.email} successfully!")
163+
logging.warning(f"User {user.email} deleted author {author.email} successfully!")
161164
await flash(f"Author {author.firstname}, {author.lastname} deleted successfully!", "success")
162165
except ValidationError as err:
163166
errors = err.messages
164167
valid_data = err.valid_data
165168
print(f"Failed to delete author {id} error! {errors}")
166-
logging.error(f"User {session['user']['email']} failed to delete author {id}! Exception: {errors}")
169+
logging.error(f"User {user.email} failed to delete author {id}! Exception: {errors}")
167170
await flash(f"Failed to delete author {id}! Exception: {errors}", "danger")
168171
return redirect(url_for("user.index"))

0 commit comments

Comments
 (0)