Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions django_email_learning/services/jwt_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.conf import settings
from datetime import datetime, timedelta
import jwt

SECRET = settings.SECRET_KEY
ALGORITHM = "HS256"


def generate_jwt(payload: dict, expiration_seconds: int = 3600) -> str:
payload_copy = payload.copy()
payload_copy["exp"] = datetime.utcnow() + timedelta(seconds=expiration_seconds)
token = jwt.encode(payload_copy, SECRET, algorithm=ALGORITHM)
return token


def decode_jwt(token: str) -> dict:
decoded = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
return decoded
2 changes: 1 addition & 1 deletion frontend/course/components/ContentTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
onClick={() => {let event = {type: 'content_clicked', content_id: content.id}; eventHandler(event);}}
color='primary.dark' sx={{ cursor: 'pointer'}}>{content.title}</Typography></TableCell>
<TableCell>{formatPeriod(content.waiting_period)}</TableCell>
<TableCell>{content.type}</TableCell>
<TableCell>{content.type.charAt(0).toUpperCase() + content.type.slice(1)}</TableCell>
<TableCell><Switch defaultChecked={content.is_published} onChange={() => TogglePublishContent(content.id, !content.is_published)} disabled={userRole == 'viewer'} /></TableCell>
{userRole !== 'viewer' && <TableCell align='right'>
<IconButton aria-label="delete" onClick={() => deleteContent(content.id)}>
Expand Down
44 changes: 3 additions & 41 deletions frontend/src/components/MenuBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza
}

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


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

return (

// return (<AppBar sx={{boxShadow: 0, backgroundColor: 'white', borderBottom: '1px solid', borderColor: 'primary.main'}}>
// <Toolbar>
// <Box ml={2}>
// <img src={logoUrl} alt="Logo" style={{ height: 36 }} />
// </Box>
// <Typography variant="body1" component="span" sx={{ flexGrow: 1, ml: 2, color: 'primary.dark' }}>
// Email Learning
// {
// showOrganizationSwitcher && organizations.length > 0 && <OrganizationsSelect organizations={organizations} activeOrganizationId={activeOrganizationId} changeOrganizationCallback={changeOrganizationCallback} sx={{ display: { xs: 'none', md: 'inline-grid' } }} />
// }
// </Typography>
// <ThemeSwitcher />
// <Box sx={{display: { xs: 'flex', md: 'none'}, right: 0, position: "absolute" }}>
// <IconButton
// size="large"
// aria-label="account of current user"
// aria-controls="menu-appbar"
// aria-haspopup="true"
// onClick={toggleMenuDrawer(true)}
// color="primary"
// >
// <MenuIcon />
// </IconButton>
// </Box>

// <Box sx={{ float: "right", display: { xs: 'none', md: 'flex' } }}>
// {pages.map((page) => (
// <Button
// key={page.name}
// href={page.href}
// sx={{ color: 'black', display: 'block', textTransform: 'none' }}
// >
// {page.name}
// </Button>
// ))}
// </Box>
// </Toolbar>
<Box component="nav"sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}>
<AppBar sx={{boxShadow: 0, backgroundColor: 'background.nav', borderBottom: {xs: '1px solid', md: 'none'}, borderColor: {xs: 'primary.main', md: 'none'} }}>
<Box my={1} ml={5}>
<img src={logoHorizontalUrl} alt="Logo" style={{ height: 57 }} />
<Box my={1} ml={5} sx={{ height: {xs: "57px", md: "30px"}}}>
<img src={logoHorizontalUrl} alt="Logo" style={{maxHeight: "57px", height: "100%"}} />
</Box>
<Box sx={{display: { xs: 'flex'}, right: 0, position: "absolute" }}>
<ThemeSwitcher />
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ThemeSwitcher.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ThemeSwitcher = () => {
};

return (
<Box sx={{ pt: '19px' }}>
<Box sx={{ pt: {xs: '19px', md: '10px'} }}>
<DayNightSwitch checked={!isLightTheme} onChange={toggleTheme} />
</Box>
);
Expand Down
62 changes: 61 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "django-email-learning"
version = "0.1.13"
version = "0.1.14"
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."
authors = [
{name = "Payam Najafizadeh",email = "[email protected]"}
Expand All @@ -13,6 +13,7 @@ dependencies = [
"cryptography (>=46.0.3,<47.0.0)",
"pillow (>=12.0.0,<13.0.0)",
"pydantic (>=2.12.4,<3.0.0)",
"pyjwt (>=2.10.1,<3.0.0)",
]
classifiers = [
"Development Status :: 3 - Alpha",
Expand Down Expand Up @@ -43,7 +44,8 @@ dev = [
"django-cors-headers (>=4.9.0,<5.0.0)",
"pre-commit (>=4.4.0,<5.0.0)",
"bandit (>=1.8.6,<2.0.0)",
"pytest-cov (>=7.0.0,<8.0.0)"
"pytest-cov (>=7.0.0,<8.0.0)",
"freezegun (>=1.5.5,<2.0.0)"
]

[build-system]
Expand Down
41 changes: 41 additions & 0 deletions tests/services/test_jwt_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django_email_learning.services import jwt_service
from freezegun import freeze_time
from datetime import timedelta, datetime
import jwt
import pytest


def test_jwt_service_generate_and_decode_jwt():
payload = {"user_id": 123, "email": "[email protected]"}
token = jwt_service.generate_jwt(payload)
decoded_payload = jwt_service.decode_jwt(token)
assert decoded_payload["user_id"] == payload["user_id"]
assert decoded_payload["email"] == payload["email"]


def test_jwt_service_token_expiration():
payload = {"user_id": 456}

# Freeze time at a specific moment
with freeze_time("2023-01-01 12:00:00") as frozen_time:
token = jwt_service.generate_jwt(payload, expiration_seconds=3600)

# Fast forward time by 4000 seconds
frozen_time.tick(delta=timedelta(seconds=4000))

# Token should now be expired
with pytest.raises(jwt.ExpiredSignatureError):
jwt_service.decode_jwt(token)


def test_jwt_service_invalid_token():
payload = {"user_id": 789}
payload_copy = payload.copy()
payload_copy["exp"] = datetime.utcnow() + timedelta(seconds=3600)
# Create an invalid token by altering the signature
invalid_token = jwt.encode(
payload_copy, "INVALID_SECRET", algorithm=jwt_service.ALGORITHM
)

with pytest.raises(jwt.InvalidSignatureError):
jwt_service.decode_jwt(invalid_token)