Skip to content

Commit f6e4a49

Browse files
committed
(PC-38964)[API] chore: improve user management
1 parent 4c81ea7 commit f6e4a49

File tree

9 files changed

+76
-14
lines changed

9 files changed

+76
-14
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
6f2f704ec485 (pre) (head)
1+
44890ce1925d (pre) (head)
22
b7e8461b1da3 (post) (head)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""update session schema"""
2+
3+
import sqlalchemy as sa
4+
from alembic import op
5+
6+
7+
# pre/post deployment: pre
8+
# revision identifiers, used by Alembic.
9+
revision = "44890ce1925d"
10+
down_revision = "6f2f704ec485"
11+
branch_labels: tuple[str] | None = None
12+
depends_on: list[str] | None = None
13+
14+
15+
def upgrade() -> None:
16+
op.add_column("user_session", sa.Column("expirationDatetime", sa.DateTime(), nullable=True))
17+
18+
19+
def downgrade() -> None:
20+
op.drop_column("user_session", "expirationDatetime")

api/src/pcapi/core/users/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,7 @@ class UserSession(PcObject, Model):
12801280
__tablename__ = "user_session"
12811281
userId: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.BigInteger, nullable=False)
12821282
uuid: sa_orm.Mapped[UUID] = sa_orm.mapped_column(postgresql.UUID(as_uuid=True), unique=True, nullable=False)
1283+
expirationDatetime: sa_orm.Mapped[datetime] = sa_orm.mapped_column(sa.DateTime, nullable=True)
12831284

12841285

12851286
class TrustedDevice(PcObject, Model):

api/src/pcapi/routes/backoffice/auth.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
from . import utils
2727

2828

29+
# a french worker cannot work for more than 12 hours so disconnecting them after 13 hours is safe
30+
MAXIMUM_SESSION_LENGTH_HOURS = 13
31+
2932
logger = logging.getLogger(__name__)
3033

3134

@@ -50,7 +53,7 @@ def login() -> utils.BackofficeResponse:
5053
db.session.flush()
5154

5255
login_user(local_admin, remember=True)
53-
login_manager.stamp_session(local_admin)
56+
login_manager.stamp_session(user=local_admin, duration=datetime.timedelta(hours=MAXIMUM_SESSION_LENGTH_HOURS))
5457
return werkzeug.utils.redirect(url_for(".home"))
5558

5659
redirect_uri = url_for(".authorize", _external=True)
@@ -111,13 +114,16 @@ def authorize() -> utils.BackofficeResponse:
111114
)
112115

113116
login_user(user, remember=True)
114-
login_manager.stamp_session(user)
117+
login_manager.stamp_session(user=user, duration=datetime.timedelta(hours=MAXIMUM_SESSION_LENGTH_HOURS))
115118
return redirect(url_for(".home"))
116119

117120

118121
@blueprint.backoffice_web.route("/logout", methods=["POST"])
119122
@utils.custom_login_required(redirect_to=".home")
120123
def logout() -> utils.BackofficeResponse:
124+
from pcapi.utils import login_manager
125+
126+
login_manager.discard_session()
121127
logout_user()
122128
return redirect(url_for(".home"), code=303)
123129

api/src/pcapi/routes/pro/users.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ def validate_user(token: str) -> None:
8585
raise ResourceNotFoundError(errors={"global": "Le lien est invalide ou a expiré. Veuillez recommencer."})
8686
discard_session()
8787
login_user(user)
88-
stamp_session(user)
88+
stamp_session(
89+
user=user,
90+
duration=settings.PRO_SESSION_FORCE_TIMEOUT_IN_DAYS,
91+
)
8992
flask.session["last_login"] = date_utils.get_naive_utc_now().timestamp()
9093
users_api.update_last_connection_date(user)
9194

@@ -259,7 +262,10 @@ def signin(body: users_serializers.LoginUserBodyModel) -> users_serializers.Shar
259262

260263
discard_session()
261264
login_user(user)
262-
stamp_session(user)
265+
stamp_session(
266+
user=user,
267+
duration=settings.PRO_SESSION_FORCE_TIMEOUT_IN_DAYS,
268+
)
263269
flask.session["last_login"] = date_utils.get_naive_utc_now().timestamp()
264270
users_api.update_last_connection_date(user)
265271

@@ -341,7 +347,10 @@ def connect_as(token: str) -> Response:
341347
discard_session()
342348
logout_user()
343349
login_user(user)
344-
stamp_session(user)
350+
stamp_session(
351+
user=user,
352+
duration=settings.PRO_SESSION_FORCE_TIMEOUT_IN_DAYS,
353+
)
345354
flask.session["internal_admin_email"] = token_data.internal_admin_email
346355
flask.session["internal_admin_id"] = token_data.internal_admin_id
347356
flask.session["last_login"] = date_utils.get_naive_utc_now().timestamp()

api/src/pcapi/utils/login_manager.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
22
import uuid
33
from datetime import datetime
4+
from datetime import timedelta
45

56
import flask
7+
import sqlalchemy as sa
68
import werkzeug.datastructures
79
from flask import current_app as app
810
from flask_login import logout_user
@@ -43,7 +45,10 @@ def get_user_with_id(user_id: str) -> users_models.User | None:
4345

4446
flask.session.permanent = True
4547
session_uuid = flask.session.get("session_uuid")
46-
user_session = db.session.query(users_models.UserSession).filter_by(userId=user_id, uuid=session_uuid).one_or_none()
48+
user_session = get_session(
49+
user_id=user_id,
50+
session_uuid=session_uuid,
51+
)
4752

4853
if not user_session:
4954
return None
@@ -70,11 +75,32 @@ def send_401() -> tuple[flask.Response, int]:
7075
return flask.jsonify(e.errors), 401
7176

7277

73-
def stamp_session(user: users_models.User) -> None:
78+
def get_session(user_id: str, session_uuid: str | None) -> users_models.UserSession | None:
79+
if not user_id or not session_uuid:
80+
return None
81+
return (
82+
db.session.query(users_models.UserSession)
83+
.filter(
84+
users_models.UserSession.userId == int(user_id),
85+
users_models.UserSession.uuid == session_uuid,
86+
sa.or_(
87+
users_models.UserSession.expirationDatetime > date_utils.get_naive_utc_now(),
88+
users_models.UserSession.expirationDatetime == None,
89+
),
90+
)
91+
.one_or_none()
92+
)
93+
94+
95+
def stamp_session(user: users_models.User, duration: timedelta) -> None:
7496
session_uuid = uuid.uuid4()
7597
flask.session["session_uuid"] = session_uuid
7698
flask.session["user_id"] = user.id
77-
db.session.add(users_models.UserSession(userId=user.id, uuid=session_uuid))
99+
db.session.add(
100+
users_models.UserSession(
101+
userId=user.id, uuid=session_uuid, expirationDatetime=date_utils.get_naive_utc_now() + duration
102+
)
103+
)
78104
db.session.commit()
79105

80106

@@ -101,6 +127,7 @@ def manage_pro_session(user: users_models.User | None) -> users_models.User | No
101127
current_timestamp = date_utils.get_naive_utc_now().timestamp()
102128
last_login = datetime.fromtimestamp(flask.session.get("last_login", current_timestamp))
103129
last_api_call = datetime.fromtimestamp(flask.session.get("last_api_call", current_timestamp))
130+
104131
valid_session = compute_pro_session_validity(last_login, last_api_call)
105132

106133
if "last_login" not in flask.session:

api/tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import typing
99
import urllib.parse
1010
from dataclasses import dataclass
11+
from datetime import timedelta
1112
from pathlib import Path
1213
from pprint import pprint
1314
from unittest.mock import MagicMock
@@ -109,7 +110,7 @@ def signin(user_id: int):
109110
user = db.session.query(User).filter_by(id=user_id).one()
110111

111112
login_user(user, remember=True)
112-
login_manager.stamp_session(user)
113+
login_manager.stamp_session(user, timedelta(minutes=15))
113114

114115
return ""
115116

api/tests/routes/native/v1/account_test.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,7 @@ def test_num_queries_with_next_step(self, client):
370370
response = client.get("/native/v1/me")
371371
assert response.status_code == 200
372372
client.with_token(user.email)
373-
n_queries = 1 # get user session
374-
n_queries += 1 # get user
373+
n_queries = 1 # get user
375374
n_queries += 1 # get bookings
376375

377376
with assert_num_queries(n_queries):

api/tests/routes/native/v1/favorites_test.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,7 @@ def test_uses_offerer_address_if_available(self, client):
345345
client.post(FAVORITES_URL, json={"offerId": offer.id})
346346
# 1: Fetch the user for auth
347347
# 1: Fetch the favorites
348-
num_queries = 1 # Get session
349-
num_queries += 1 # Get user
348+
num_queries = 1 # Get user
350349
num_queries += 1 # Get favorites
351350
with assert_num_queries(num_queries):
352351
response = client.get(FAVORITES_URL)

0 commit comments

Comments
 (0)