Skip to content
This repository was archived by the owner on May 26, 2025. It is now read-only.

Commit 755d70f

Browse files
authored
Expanded user coverage (#22)
1 parent 687cb57 commit 755d70f

File tree

9 files changed

+382
-14
lines changed

9 files changed

+382
-14
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ release: ## Create a new tag for release.
9191
docs: ## Build the documentation.
9292
@echo "building documentation ..."
9393
@$(ENV_PREFIX)mkdocs build
94-
URL="site/index.html"; xdg-open $$URL || sensible-browser $$URL || x-www-browser $$URL || gnome-open $$URL
94+
URL="site/index.html"; xdg-open $$URL || sensible-browser $$URL || x-www-browser $$URL || gnome-open $$URL || open $$URL
9595

9696
.PHONY: switch-to-poetry
9797
switch-to-poetry: ## Switch to poetry package manager.

project_name/default.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# SECRET_KEY = ""
66
ALGORITHM = "HS256"
77
ACCESS_TOKEN_EXPIRE_MINUTES = 30
8+
REFRESH_TOKEN_EXPIRE_MINUTES = 600
89

910
[default.server]
1011
port = 8000

project_name/routes/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
from fastapi import APIRouter
22

33
from .content import router as content_router
4+
from .profile import router as profile_router
45
from .security import router as security_router
56
from .user import router as user_router
67

78
main_router = APIRouter()
89

9-
main_router.include_router(user_router, prefix="/user", tags=["user"])
1010
main_router.include_router(content_router, prefix="/content", tags=["content"])
11+
main_router.include_router(profile_router, tags=["user"])
1112
main_router.include_router(security_router, tags=["security"])
13+
main_router.include_router(user_router, prefix="/user", tags=["user"])
1214

1315

1416
@main_router.get("/")

project_name/routes/profile.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from fastapi import APIRouter
2+
3+
from ..security import AuthenticatedUser, User, UserResponse
4+
5+
router = APIRouter()
6+
7+
8+
@router.get("/profile", response_model=UserResponse)
9+
async def my_profile(current_user: User = AuthenticatedUser):
10+
return current_user

project_name/routes/security.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55

66
from ..config import settings
77
from ..security import (
8+
RefreshToken,
89
Token,
910
User,
1011
authenticate_user,
1112
create_access_token,
13+
create_refresh_token,
1214
get_user,
15+
validate_token,
1316
)
1417

1518
ACCESS_TOKEN_EXPIRE_MINUTES = settings.security.access_token_expire_minutes
16-
19+
REFRESH_TOKEN_EXPIRE_MINUTES = settings.security.refresh_token_expire_minutes
1720

1821
router = APIRouter()
1922

@@ -32,7 +35,39 @@ async def login_for_access_token(
3235

3336
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
3437
access_token = create_access_token(
35-
data={"sub": user.username}, expires_delta=access_token_expires
38+
data={"sub": user.username, "fresh": True},
39+
expires_delta=access_token_expires,
40+
)
41+
42+
refresh_token_expires = timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)
43+
refresh_token = create_refresh_token(
44+
data={"sub": user.username}, expires_delta=refresh_token_expires
45+
)
46+
47+
return {
48+
"access_token": access_token,
49+
"refresh_token": refresh_token,
50+
"token_type": "bearer",
51+
}
52+
53+
54+
@router.post("/refresh_token", response_model=Token)
55+
async def refresh_token(form_data: RefreshToken):
56+
user = await validate_token(token=form_data.refresh_token)
57+
58+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
59+
access_token = create_access_token(
60+
data={"sub": user.username, "fresh": False},
61+
expires_delta=access_token_expires,
62+
)
63+
64+
refresh_token_expires = timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)
65+
refresh_token = create_refresh_token(
66+
data={"sub": user.username}, expires_delta=refresh_token_expires
3667
)
3768

38-
return {"access_token": access_token, "token_type": "bearer"}
69+
return {
70+
"access_token": access_token,
71+
"refresh_token": refresh_token,
72+
"token_type": "bearer",
73+
}

project_name/routes/user.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ..db import ActiveSession
88
from ..security import (
99
AdminUser,
10+
AuthenticatedFreshUser,
1011
AuthenticatedUser,
1112
HashedPassword,
1213
User,
@@ -27,6 +28,15 @@ async def list_users(*, session: Session = ActiveSession):
2728

2829
@router.post("/", response_model=UserResponse, dependencies=[AdminUser])
2930
async def create_user(*, session: Session = ActiveSession, user: UserCreate):
31+
32+
# verify user with username doesn't already exist
33+
try:
34+
await query_user(session=session, user_id_or_username=user.username)
35+
except HTTPException:
36+
pass
37+
else:
38+
raise HTTPException(status_code=422, detail="Username already exists")
39+
3040
db_user = User.from_orm(user)
3141
session.add(db_user)
3242
session.commit()
@@ -37,7 +47,7 @@ async def create_user(*, session: Session = ActiveSession, user: UserCreate):
3747
@router.patch(
3848
"/{user_id}/password/",
3949
response_model=UserResponse,
40-
dependencies=[AuthenticatedUser],
50+
dependencies=[AuthenticatedFreshUser],
4151
)
4252
async def update_user_password(
4353
*,
@@ -85,16 +95,11 @@ async def query_user(
8595
)
8696
)
8797

88-
if not user:
98+
if not user.first():
8999
raise HTTPException(status_code=404, detail="User not found")
90100
return user.first()
91101

92102

93-
@router.get("/me/", response_model=UserResponse)
94-
async def my_profile(current_user: User = AuthenticatedUser):
95-
return current_user
96-
97-
98103
@router.delete("/{user_id}/", dependencies=[AdminUser])
99104
def delete_user(
100105
*, session: Session = ActiveSession, request: Request, user_id: int

project_name/security.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222

2323
class Token(BaseModel):
2424
access_token: str
25+
refresh_token: str
2526
token_type: str
2627

2728

29+
class RefreshToken(BaseModel):
30+
refresh_token: str
31+
32+
2833
class TokenData(BaseModel):
2934
username: Optional[str] = None
3035

@@ -116,7 +121,20 @@ def create_access_token(
116121
expire = datetime.utcnow() + expires_delta
117122
else:
118123
expire = datetime.utcnow() + timedelta(minutes=15)
119-
to_encode.update({"exp": expire})
124+
to_encode.update({"exp": expire, "scope": "access_token"})
125+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
126+
return encoded_jwt
127+
128+
129+
def create_refresh_token(
130+
data: dict, expires_delta: Optional[timedelta] = None
131+
) -> str:
132+
to_encode = data.copy()
133+
if expires_delta:
134+
expire = datetime.utcnow() + expires_delta
135+
else:
136+
expire = datetime.utcnow() + timedelta(minutes=15)
137+
to_encode.update({"exp": expire, "scope": "refresh_token"})
120138
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
121139
return encoded_jwt
122140

@@ -138,7 +156,7 @@ def get_user(username) -> Optional[User]:
138156

139157

140158
def get_current_user(
141-
token: str = Depends(oauth2_scheme), request: Request = None
159+
token: str = Depends(oauth2_scheme), request: Request = None, fresh=False
142160
) -> User:
143161
credentials_exception = HTTPException(
144162
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -156,6 +174,7 @@ def get_current_user(
156174
try:
157175
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
158176
username: str = payload.get("sub")
177+
159178
if username is None:
160179
raise credentials_exception
161180
token_data = TokenData(username=username)
@@ -164,6 +183,9 @@ def get_current_user(
164183
user = get_user(username=token_data.username)
165184
if user is None:
166185
raise credentials_exception
186+
if fresh and (not payload["fresh"] and not user.superuser):
187+
raise credentials_exception
188+
167189
return user
168190

169191

@@ -178,6 +200,15 @@ async def get_current_active_user(
178200
AuthenticatedUser = Depends(get_current_active_user)
179201

180202

203+
def get_current_fresh_user(
204+
token: str = Depends(oauth2_scheme), request: Request = None
205+
) -> User:
206+
return get_current_user(token, request, True)
207+
208+
209+
AuthenticatedFreshUser = Depends(get_current_fresh_user)
210+
211+
181212
async def get_current_admin_user(
182213
current_user: User = Depends(get_current_user),
183214
) -> User:
@@ -189,3 +220,9 @@ async def get_current_admin_user(
189220

190221

191222
AdminUser = Depends(get_current_admin_user)
223+
224+
225+
async def validate_token(token: str = Depends(oauth2_scheme)) -> User:
226+
227+
user = get_current_user(token=token)
228+
return user

tests/test_profile.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
def test_profile(api_client_authenticated):
2+
response = api_client_authenticated.get("/profile")
3+
assert response.status_code == 200
4+
result = response.json()
5+
assert "admin" in result["username"]
6+
7+
8+
def test_profile_no_auth(api_client):
9+
response = api_client.get("/profile")
10+
assert response.status_code == 401

0 commit comments

Comments
 (0)