Skip to content

Commit c9f6c1e

Browse files
authored
Update: Auth API (#115)
* Updated the /auth metadata for better documentation * Default time of timestamps is created by the database
1 parent 4091bb2 commit c9f6c1e

File tree

7 files changed

+69
-43
lines changed

7 files changed

+69
-43
lines changed

src/auth/crud.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from sqlalchemy.ext.asyncio import AsyncSession
66

77
from auth.tables import SiteUser, UserSession
8-
from auth.types import SiteUserData
98

109
_logger = logging.getLogger(__name__)
1110

@@ -52,7 +51,7 @@ async def create_user_session(db_session: AsyncSession, session_id: str, computi
5251
))
5352

5453

55-
async def remove_user_session(db_session: AsyncSession, session_id: str) -> dict:
54+
async def remove_user_session(db_session: AsyncSession, session_id: str):
5655
query = sqlalchemy.select(UserSession).where(UserSession.session_id == session_id)
5756
user_session = await db_session.scalars(query)
5857
await db_session.delete(user_session.first())
@@ -74,7 +73,7 @@ async def task_clean_expired_user_sessions(db_session: AsyncSession):
7473

7574

7675
# get the site user given a session ID; returns None when session is invalid
77-
async def get_site_user(db_session: AsyncSession, session_id: str) -> None | SiteUserData:
76+
async def get_site_user(db_session: AsyncSession, session_id: str) -> SiteUser | None:
7877
query = (
7978
sqlalchemy
8079
.select(UserSession)
@@ -89,17 +88,7 @@ async def get_site_user(db_session: AsyncSession, session_id: str) -> None | Sit
8988
.select(SiteUser)
9089
.where(SiteUser.computing_id == user_session.computing_id)
9190
)
92-
user = await db_session.scalar(query)
93-
if user is None:
94-
return None
95-
96-
return SiteUserData(
97-
user_session.computing_id,
98-
user.first_logged_in.isoformat(),
99-
user.last_logged_in.isoformat(),
100-
user.profile_picture_url
101-
)
102-
91+
return await db_session.scalar(query)
10392

10493
async def site_user_exists(db_session: AsyncSession, computing_id: str) -> bool:
10594
user = await db_session.scalar(

src/auth/models.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1+
from datetime import datetime
2+
13
from pydantic import BaseModel, Field
24

35

4-
class LoginBodyModel(BaseModel):
6+
class LoginBodyParams(BaseModel):
57
service: str = Field(description="Service URL used for SFU's CAS system")
68
ticket: str = Field(description="Ticket return from SFU's CAS system")
79
redirect_url: str | None = Field(None, description="Optional redirect URL")
10+
11+
class UpdateUserParams(BaseModel):
12+
profile_picture_url: str
13+
14+
class UserSessionModel(BaseModel):
15+
computing_id: str
16+
issue_time: datetime
17+
session_id: str
18+
19+
class SiteUserModel(BaseModel):
20+
computing_id: str
21+
first_logged_in: datetime
22+
last_logged_in: datetime
23+
profile_picture_url: str | None = None

src/auth/tables.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22

3-
from sqlalchemy import Column, DateTime, ForeignKey, String, Text
3+
from sqlalchemy import DateTime, ForeignKey, String, Text, func
4+
from sqlalchemy.orm import Mapped, mapped_column
45

56
from constants import COMPUTING_ID_LEN, SESSION_ID_LEN
67
from database import Base
@@ -9,17 +10,18 @@
910
class UserSession(Base):
1011
__tablename__ = "user_session"
1112

12-
computing_id = Column(
13+
computing_id: Mapped[str] = mapped_column(
1314
String(COMPUTING_ID_LEN),
1415
ForeignKey("site_user.computing_id"),
1516
# in psql pkey means non-null
1617
primary_key=True,
1718
)
1819

20+
# TODO: Make all timestamps uneditable later
1921
# time the CAS ticket was issued
20-
issue_time = Column(DateTime, nullable=False)
22+
issue_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
2123

22-
session_id = Column(
24+
session_id: Mapped[str] = mapped_column(
2325
String(SESSION_ID_LEN), nullable=False, unique=True
2426
) # the space needed to store 256 bytes in base64
2527

@@ -29,15 +31,22 @@ class SiteUser(Base):
2931
# see: https://stackoverflow.com/questions/22256124/cannot-create-a-database-table-named-user-in-postgresql
3032
__tablename__ = "site_user"
3133

32-
computing_id = Column(
34+
computing_id: Mapped[str] = mapped_column(
3335
String(COMPUTING_ID_LEN),
3436
primary_key=True,
3537
)
3638

3739
# first and last time logged into the CSSS API
38-
# note: default date (for pre-existing columns) is June 16th, 2024
39-
first_logged_in = Column(DateTime, nullable=False, default=datetime(2024, 6, 16))
40-
last_logged_in = Column(DateTime, nullable=False, default=datetime(2024, 6, 16))
40+
first_logged_in: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
41+
last_logged_in: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
4142

4243
# optional user information for display purposes
43-
profile_picture_url = Column(Text, nullable=True)
44+
profile_picture_url: Mapped[str | None] = mapped_column(Text, nullable=True)
45+
46+
def serialize(self) -> dict[str, str | int | bool | None]:
47+
return {
48+
"computing_id": self.computing_id,
49+
"first_logged_in": self.first_logged_in.isoformat(),
50+
"last_logged_in": self.last_logged_in.isoformat(),
51+
"profile_picture_url": self.profile_picture_url
52+
}

src/auth/urls.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
import requests # TODO: make this async
77
import xmltodict
88
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, Response
9-
from fastapi.responses import JSONResponse, PlainTextResponse, RedirectResponse
9+
from fastapi.responses import JSONResponse, RedirectResponse
1010

1111
import database
1212
from auth import crud
13-
from auth.models import LoginBodyModel
13+
from auth.models import LoginBodyParams, SiteUserModel, UpdateUserParams
1414
from constants import DOMAIN, IS_PROD, SAMESITE
15-
from utils.shared_models import DetailModel
15+
from utils.shared_models import DetailModel, MessageModel
1616

1717
_logger = logging.getLogger(__name__)
1818

@@ -51,7 +51,7 @@ async def login_user(
5151
request: Request,
5252
db_session: database.DBSession,
5353
background_tasks: BackgroundTasks,
54-
body: LoginBodyModel
54+
body: LoginBodyParams
5555
):
5656
# verify the ticket is valid
5757
service_url = body.service
@@ -94,8 +94,9 @@ async def login_user(
9494

9595
@router.get(
9696
"/logout",
97-
operation_id="logout",
9897
description="Logs out the current user by invalidating the session_id cookie",
98+
operation_id="logout",
99+
response_model=MessageModel
99100
)
100101
async def logout_user(
101102
request: Request,
@@ -119,6 +120,10 @@ async def logout_user(
119120
"/user",
120121
operation_id="get_user",
121122
description="Get info about the current user. Only accessible by that user",
123+
response_model=SiteUserModel,
124+
responses={
125+
401: { "description": "Not logged in.", "model": DetailModel }
126+
},
122127
)
123128
async def get_user(
124129
request: Request,
@@ -129,35 +134,38 @@ async def get_user(
129134
"""
130135
session_id = request.cookies.get("session_id", None)
131136
if session_id is None:
132-
raise HTTPException(status_code=401, detail="User must be authenticated to get their info")
137+
raise HTTPException(status_code=401, detail="user must be authenticated to get their info")
133138

134139
user_info = await crud.get_site_user(db_session, session_id)
135140
if user_info is None:
136-
raise HTTPException(status_code=401, detail="Could not find user with session_id, please log in")
141+
raise HTTPException(status_code=401, detail="could not find user with session_id, please log in")
137142

138-
return JSONResponse(user_info.serializable_dict())
143+
return JSONResponse(user_info.serialize())
139144

140145

146+
# TODO: We should change this so that the admins can change people's pictures too, so they can remove offensive stuff
141147
@router.patch(
142148
"/user",
143149
operation_id="update_user",
144150
description="Update information for the currently logged in user. Only accessible by that user",
151+
response_model=str,
152+
responses={
153+
401: { "description": "Not logged in.", "model": DetailModel }
154+
},
145155
)
146156
async def update_user(
147-
profile_picture_url: str,
157+
body: UpdateUserParams,
148158
request: Request,
149159
db_session: database.DBSession,
150160
):
151161
"""
152162
Returns the info stored in the site_user table in the auth module, if the user is logged in.
153163
"""
154-
session_id = request.cookies.get("session_id", None)
164+
session_id = request.cookies.get("session_id")
155165
if session_id is None:
156-
raise HTTPException(status_code=401, detail="User must be authenticated to get their info")
166+
raise HTTPException(status_code=401, detail="user must be authenticated to get their info")
157167

158-
ok = await crud.update_site_user(db_session, session_id, profile_picture_url)
168+
ok = await crud.update_site_user(db_session, session_id, body.profile_picture_url)
159169
await db_session.commit()
160170
if not ok:
161-
raise HTTPException(status_code=401, detail="Could not find user with session_id, please log in")
162-
163-
return PlainTextResponse("ok")
171+
raise HTTPException(status_code=401, detail="could not find user with session_id, please log in")

src/database.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
AsyncConnection,
1313
AsyncSession,
1414
)
15+
from sqlalchemy.orm import DeclarativeBase
1516

1617
convention = {
1718
"ix": "ix_%(column_0_label)s", # index
@@ -21,8 +22,8 @@
2122
"pk": "pk_%(table_name)s", # primary key
2223
}
2324

24-
Base = sqlalchemy.orm.declarative_base()
25-
Base.metadata = MetaData(naming_convention=convention)
25+
class Base(DeclarativeBase):
26+
metadata = MetaData(naming_convention=convention)
2627

2728
# from: https://medium.com/@tclaitken/setting-up-a-fastapi-app-with-async-sqlalchemy-2-0-pydantic-v2-e6c540be4308
2829
class DatabaseSessionManager:

src/elections/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from enum import Enum
1+
from enum import StrEnum
22

33
from pydantic import BaseModel
44

55

6-
class ElectionTypeEnum(str, Enum):
6+
class ElectionTypeEnum(StrEnum):
77
GENERAL = "general_election"
88
BY_ELECTION = "by_election"
99
COUNCIL_REP = "council_rep_election"

src/utils/shared_models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ class SuccessFailModel(BaseModel):
66

77
class DetailModel(BaseModel):
88
detail: str
9+
10+
class MessageModel(BaseModel):
11+
message: str

0 commit comments

Comments
 (0)