diff --git a/django_email_learning/services/jwt_service.py b/django_email_learning/services/jwt_service.py
new file mode 100644
index 0000000..22091d4
--- /dev/null
+++ b/django_email_learning/services/jwt_service.py
@@ -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
diff --git a/frontend/course/components/ContentTable.jsx b/frontend/course/components/ContentTable.jsx
index 6744be1..bc9043e 100644
--- a/frontend/course/components/ContentTable.jsx
+++ b/frontend/course/components/ContentTable.jsx
@@ -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}
{formatPeriod(content.waiting_period)}
- {content.type}
+ {content.type.charAt(0).toUpperCase() + content.type.slice(1)}
TogglePublishContent(content.id, !content.is_published)} disabled={userRole == 'viewer'} />
{userRole !== 'viewer' &&
deleteContent(content.id)}>
diff --git a/frontend/src/components/MenuBar.jsx b/frontend/src/components/MenuBar.jsx
index 9efc490..70e1fc3 100644
--- a/frontend/src/components/MenuBar.jsx
+++ b/frontend/src/components/MenuBar.jsx
@@ -86,7 +86,7 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza
}
pages.push({ name: 'Course Management', icon: , href: platformBaseUrl + '/courses/' });
- pages.push({ name: 'Users', icon: , href: platformBaseUrl + '/users/' });
+ pages.push({ name: 'Learners', icon: , href: platformBaseUrl + '/users/' });
pages.push({ name: 'Analytics', icon: , href: platformBaseUrl + '/analytics/' });
@@ -95,48 +95,10 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza
};
return (
-
- // return (
- //
- //
- //
- //
- //
- // Email Learning
- // {
- // showOrganizationSwitcher && organizations.length > 0 &&
- // }
- //
- //
- //
- //
- //
- //
- //
-
- //
- // {pages.map((page) => (
- //
- // ))}
- //
- //
-
-
+
+
diff --git a/frontend/src/components/ThemeSwitcher.jsx b/frontend/src/components/ThemeSwitcher.jsx
index 12fae67..e0f0308 100644
--- a/frontend/src/components/ThemeSwitcher.jsx
+++ b/frontend/src/components/ThemeSwitcher.jsx
@@ -72,7 +72,7 @@ const ThemeSwitcher = () => {
};
return (
-
+
);
diff --git a/poetry.lock b/poetry.lock
index e92312e..68f10b7 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -525,6 +525,21 @@ files = [
{file = "filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"},
]
+[[package]]
+name = "freezegun"
+version = "1.5.5"
+description = "Let your Python tests travel through time"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2"},
+ {file = "freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.7"
+
[[package]]
name = "identify"
version = "2.6.15"
@@ -1130,6 +1145,24 @@ files = [
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
+[[package]]
+name = "pyjwt"
+version = "2.10.1"
+description = "JSON Web Token implementation in Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
+ {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
+]
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
[[package]]
name = "pytest"
version = "9.0.2"
@@ -1193,6 +1226,21 @@ pytest = ">=7.0.0"
docs = ["sphinx", "sphinx_rtd_theme"]
testing = ["Django", "django-configurations (>=2.0)"]
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["dev"]
+files = [
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
[[package]]
name = "pyyaml"
version = "6.0.3"
@@ -1324,6 +1372,18 @@ files = [
{file = "ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4"},
]
+[[package]]
+name = "six"
+version = "1.17.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["dev"]
+files = [
+ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
+ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
+]
+
[[package]]
name = "sqlparse"
version = "0.5.4"
@@ -1482,4 +1542,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata]
lock-version = "2.1"
python-versions = ">=3.10"
-content-hash = "1a5e66f72edcc170e089b5e9b163c3b90aa88fe96dece215bd14bf147952988b"
+content-hash = "5941696826834e3934e3b0f2fb8c74ebcb46719d809d2637842d6f421a5ea926"
diff --git a/pyproject.toml b/pyproject.toml
index b638eb6..92f2913 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 = "payam.nj@gmail.com"}
@@ -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",
@@ -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]
diff --git a/tests/services/test_jwt_service.py b/tests/services/test_jwt_service.py
new file mode 100644
index 0000000..d174300
--- /dev/null
+++ b/tests/services/test_jwt_service.py
@@ -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": "test@example.com"}
+ 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)