Skip to content

Commit 73e98f7

Browse files
authored
Merge pull request #104 from AvaCodeSolutions/feat/24/jwt-service
Feat/24/jwt service
2 parents e952a53 + 4fcaf08 commit 73e98f7

7 files changed

Lines changed: 129 additions & 46 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from django.conf import settings
2+
from datetime import datetime, timedelta
3+
import jwt
4+
5+
SECRET = settings.SECRET_KEY
6+
ALGORITHM = "HS256"
7+
8+
9+
def generate_jwt(payload: dict, expiration_seconds: int = 3600) -> str:
10+
payload_copy = payload.copy()
11+
payload_copy["exp"] = datetime.utcnow() + timedelta(seconds=expiration_seconds)
12+
token = jwt.encode(payload_copy, SECRET, algorithm=ALGORITHM)
13+
return token
14+
15+
16+
def decode_jwt(token: str) -> dict:
17+
decoded = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
18+
return decoded

frontend/course/components/ContentTable.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
147147
onClick={() => {let event = {type: 'content_clicked', content_id: content.id}; eventHandler(event);}}
148148
color='primary.dark' sx={{ cursor: 'pointer'}}>{content.title}</Typography></TableCell>
149149
<TableCell>{formatPeriod(content.waiting_period)}</TableCell>
150-
<TableCell>{content.type}</TableCell>
150+
<TableCell>{content.type.charAt(0).toUpperCase() + content.type.slice(1)}</TableCell>
151151
<TableCell><Switch defaultChecked={content.is_published} onChange={() => TogglePublishContent(content.id, !content.is_published)} disabled={userRole == 'viewer'} /></TableCell>
152152
{userRole !== 'viewer' && <TableCell align='right'>
153153
<IconButton aria-label="delete" onClick={() => deleteContent(content.id)}>

frontend/src/components/MenuBar.jsx

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza
8686
}
8787

8888
pages.push({ name: 'Course Management', icon: <SchoolIcon fontSize="small" />, href: platformBaseUrl + '/courses/' });
89-
pages.push({ name: 'Users', icon: <PeopleIcon fontSize="small" />, href: platformBaseUrl + '/users/' });
89+
pages.push({ name: 'Learners', icon: <PeopleIcon fontSize="small" />, href: platformBaseUrl + '/users/' });
9090
pages.push({ name: 'Analytics', icon: <BarChartIcon fontSize="small" />, href: platformBaseUrl + '/analytics/' });
9191

9292

@@ -95,48 +95,10 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza
9595
};
9696

9797
return (
98-
99-
// return (<AppBar sx={{boxShadow: 0, backgroundColor: 'white', borderBottom: '1px solid', borderColor: 'primary.main'}}>
100-
// <Toolbar>
101-
// <Box ml={2}>
102-
// <img src={logoUrl} alt="Logo" style={{ height: 36 }} />
103-
// </Box>
104-
// <Typography variant="body1" component="span" sx={{ flexGrow: 1, ml: 2, color: 'primary.dark' }}>
105-
// Email Learning
106-
// {
107-
// showOrganizationSwitcher && organizations.length > 0 && <OrganizationsSelect organizations={organizations} activeOrganizationId={activeOrganizationId} changeOrganizationCallback={changeOrganizationCallback} sx={{ display: { xs: 'none', md: 'inline-grid' } }} />
108-
// }
109-
// </Typography>
110-
// <ThemeSwitcher />
111-
// <Box sx={{display: { xs: 'flex', md: 'none'}, right: 0, position: "absolute" }}>
112-
// <IconButton
113-
// size="large"
114-
// aria-label="account of current user"
115-
// aria-controls="menu-appbar"
116-
// aria-haspopup="true"
117-
// onClick={toggleMenuDrawer(true)}
118-
// color="primary"
119-
// >
120-
// <MenuIcon />
121-
// </IconButton>
122-
// </Box>
123-
124-
// <Box sx={{ float: "right", display: { xs: 'none', md: 'flex' } }}>
125-
// {pages.map((page) => (
126-
// <Button
127-
// key={page.name}
128-
// href={page.href}
129-
// sx={{ color: 'black', display: 'block', textTransform: 'none' }}
130-
// >
131-
// {page.name}
132-
// </Button>
133-
// ))}
134-
// </Box>
135-
// </Toolbar>
13698
<Box component="nav"sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}>
13799
<AppBar sx={{boxShadow: 0, backgroundColor: 'background.nav', borderBottom: {xs: '1px solid', md: 'none'}, borderColor: {xs: 'primary.main', md: 'none'} }}>
138-
<Box my={1} ml={5}>
139-
<img src={logoHorizontalUrl} alt="Logo" style={{ height: 57 }} />
100+
<Box my={1} ml={5} sx={{ height: {xs: "57px", md: "30px"}}}>
101+
<img src={logoHorizontalUrl} alt="Logo" style={{maxHeight: "57px", height: "100%"}} />
140102
</Box>
141103
<Box sx={{display: { xs: 'flex'}, right: 0, position: "absolute" }}>
142104
<ThemeSwitcher />

frontend/src/components/ThemeSwitcher.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const ThemeSwitcher = () => {
7272
};
7373

7474
return (
75-
<Box sx={{ pt: '19px' }}>
75+
<Box sx={{ pt: {xs: '19px', md: '10px'} }}>
7676
<DayNightSwitch checked={!isLightTheme} onChange={toggleTheme} />
7777
</Box>
7878
);

poetry.lock

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

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "django-email-learning"
3-
version = "0.1.13"
3+
version = "0.1.14"
44
description = "A platform for creating and delivering learning materials via email within a Django application. It provides tools for content management, user role-based administration, and scheduler integration for automated content delivery."
55
authors = [
66
{name = "Payam Najafizadeh",email = "payam.nj@gmail.com"}
@@ -13,6 +13,7 @@ dependencies = [
1313
"cryptography (>=46.0.3,<47.0.0)",
1414
"pillow (>=12.0.0,<13.0.0)",
1515
"pydantic (>=2.12.4,<3.0.0)",
16+
"pyjwt (>=2.10.1,<3.0.0)",
1617
]
1718
classifiers = [
1819
"Development Status :: 3 - Alpha",
@@ -43,7 +44,8 @@ dev = [
4344
"django-cors-headers (>=4.9.0,<5.0.0)",
4445
"pre-commit (>=4.4.0,<5.0.0)",
4546
"bandit (>=1.8.6,<2.0.0)",
46-
"pytest-cov (>=7.0.0,<8.0.0)"
47+
"pytest-cov (>=7.0.0,<8.0.0)",
48+
"freezegun (>=1.5.5,<2.0.0)"
4749
]
4850

4951
[build-system]

tests/services/test_jwt_service.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from django_email_learning.services import jwt_service
2+
from freezegun import freeze_time
3+
from datetime import timedelta, datetime
4+
import jwt
5+
import pytest
6+
7+
8+
def test_jwt_service_generate_and_decode_jwt():
9+
payload = {"user_id": 123, "email": "test@example.com"}
10+
token = jwt_service.generate_jwt(payload)
11+
decoded_payload = jwt_service.decode_jwt(token)
12+
assert decoded_payload["user_id"] == payload["user_id"]
13+
assert decoded_payload["email"] == payload["email"]
14+
15+
16+
def test_jwt_service_token_expiration():
17+
payload = {"user_id": 456}
18+
19+
# Freeze time at a specific moment
20+
with freeze_time("2023-01-01 12:00:00") as frozen_time:
21+
token = jwt_service.generate_jwt(payload, expiration_seconds=3600)
22+
23+
# Fast forward time by 4000 seconds
24+
frozen_time.tick(delta=timedelta(seconds=4000))
25+
26+
# Token should now be expired
27+
with pytest.raises(jwt.ExpiredSignatureError):
28+
jwt_service.decode_jwt(token)
29+
30+
31+
def test_jwt_service_invalid_token():
32+
payload = {"user_id": 789}
33+
payload_copy = payload.copy()
34+
payload_copy["exp"] = datetime.utcnow() + timedelta(seconds=3600)
35+
# Create an invalid token by altering the signature
36+
invalid_token = jwt.encode(
37+
payload_copy, "INVALID_SECRET", algorithm=jwt_service.ALGORITHM
38+
)
39+
40+
with pytest.raises(jwt.InvalidSignatureError):
41+
jwt_service.decode_jwt(invalid_token)

0 commit comments

Comments
 (0)