ekiosk is a Django REST Framework (DRF) application for managing an online kiosk. It features authentication via OpenID Connect for customers and Django’s built-in admin for staff users, customer order management, and automated email and SMS notifications. Background tasks are handled efficiently using Celery with Redis as the message broker. The project is fully containerized with Docker and includes a CI/CD pipeline powered by GitHub Actions, with support for deployment to Kubernetes clusters for scalability and reliability.
- Customer and Order Management: Manage customers, product categories, products and orders.
- Authentication: Secure login and user management using OpenID Connect for customers, while staff users (admins) manage the system through Django’s built-in admin interface.
- Notifications: Email and SMS notifications powered by Google SMTP and Africa's Talking, with logs stored in the Notification model for verification.
- Permissions:
- Customers: Create and view their own orders.
- Admins: View all orders, update order status (PATCH), but cannot delete or modify other details, create and modify products and categories.
- Background Tasks: Asynchronous email and SMS notifications using Celery and Redis.
- Testing: Comprehensive tests for serializers, models, views, serializers and tasks using
pytest
with function-based tests and mocking. - CI/CD: GitHub Actions pipeline for building, testing, and deploying the application.
- Deployment: Dockerized with Kubernetes support for production deployment.
- Backend: Django REST Framework
- Database: PostgreSQL (via Docker Compose)
- Task Queue: Celery with Redis as the message broker(via Docker Compose)
- Notifications: Email and SMS with pluggable services
- Containerization: Docker for both development and production environments
- CI/CD: GitHub Actions pipeline
- Deployment: Kubernetes (Minikube for local testing, production-ready configurations available)
Ensure you have the following installed:
- Docker
- Docker Compose
- Git
- Python 3.9 or higher (optional, for local development without Docker)
Create a .env
file in the root directory and add the following variables:
DEBUG=
SECRET_KEY=
DJANGO_ALLOWED_HOSTS=
DB_ENGINE=
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_HOST=
POSTGRES_PORT=
OIDC_RP_CLIENT_ID=
OIDC_RP_CLIENT_SECRET=
OIDC_RP_SIGN_ALGO=RS256
ATSK_API_KEY=
DEFAULT_FROM_EMAIL=
EMAIL_HOST_PASSWORD=
EMAIL_HOST_USER=
-
Clone the repository:
git clone <repository-url> cd ekiosk
-
Build and run the Docker containers:
docker-compose up --build
-
Apply database migrations:
docker-compose exec api python manage.py migrate
-
Access the application at
http://localhost:8000/api/v1
.
Run the tests with:
docker-compose exec api pytest
This section describes the main models in the application and their relationships.
- Stores customer information, including OpenID Connect (OIDC) identifiers.
- Key Fields:
role
,email
,phone_number
,oidc_identifier(openid_sub)
- Note: Customers must have a phone number to complete their profile.
- Represents product categories, supporting up to three levels of nesting.
- Key Fields:
name
,parent_category
- Average price calculations are limited to three levels to maintain performance and relevance.
- Stores details about each product.
- Key Fields:
name
,price
,category
,stock``,discount_price
Discount price field is meant to account for price changes in case of discounts
-
Tracks customer orders.
-
Key Fields:
customer
,status
,created_at
,total_price
-
Status Workflow:
-
PENDING
→ Stock is not deducted yet, awaiting admin approval. -
COMPLETED
→ Stock is deducted after admin approval. -
CANCELED
→ Order is rejected (e.g., due to insufficient stock), and the customer is notified. -
Stock is deducted only upon admin order approval.
-
Represents individual items within an order, capturing details like product, quantity, and price at the time of purchase.
-
Key Fields:
order
,product
,quantity
,price_at_time_of_order
-
Business Logic:
- Ensures
quantity > 0
when saving the record. - The
price_at_time_of_order
is captured to prevent future price changes from affecting past orders.
- Ensures
- Logs all customer and admin notifications (email/SMS).
- Key Fields: ,
message
,created_at
Order processing in this application ensures a streamlined workflow from placement to completion while maintaining transparency and efficient communication with both admins and customers.
- The system checks stock availability for all ordered items.
- The order status is initially set to
PENDING
. - Stock is NOT deducted yet—it remains reserved, awaiting admin approval.
- Admin is notified via email about the new order.
- Customer is notified of the order placement via sms.
- Once the admin approves the order, stock is deducted from the inventory.
- The order status is updated to
COMPLETED
. - Customer is notified via SMS about the order approval and completion.
- If the admin rejects the order (e.g., due to insufficient stock or invalid details), the order is marked as
CANCELED
. - Customer is notified of the cancellation via sms.
The notification system is designed to keep customers and the admin informed at key points during the order lifecycle. Notifications are sent through Email and SMS channels to ensure timely communication.
- Email Provider: Google SMTP
- SMS Provider: Africa's Talking
Notifications are logged in the Notification
model for tracking purposes.
Below are the scenarios that trigger notifications, the notification type, and the intended recipients:
Trigger | Notification Type | Recipient |
---|---|---|
Order placed | Admin | |
Order placed | SMS | Customer |
Order approved | SMS | Customer |
Order cancelled | SMS | Customer |
-
To Admin (Email)
- "A new order (#123) placed for john@example.com requires your attention."
-
To Customer (SMS)
- "Hello +254711223344 your order #123 has been placed successfully."
- To Customer (SMS)
- "Good news, +254711223344! Your order #123 has been approved."
- To Customer (SMS)
- "Sorry, +254711223344. Your order #123 has been cancelled."
All notifications are tracked and logged in the Notification
model, including the message content and timestamp for auditing purposes.
This application provides a RESTful API for managing orders, products, and categories. Below is a summary of the available endpoints:
Root API Endpoint: api/v1
These endpoints manage authentication for customers using OIDC (OpenID Connect).
- Login with OIDC:
POST /api/v1/oidc/authenticate/
Initiates the OIDC authentication process and redirects to the identity provider. - OIDC Callback:
GET /api/v1/oidc/callback/
Handles the callback from the identity provider and processes the authentication response. - Check Profile Completion:
GET /api/v1/update-profile/
Redirects the customer to update their profile if any required fields (e.g., phone number) are missing.
- Admins use the Django Admin Interface for authentication and user management.
Access the admin panel at:/admin/
- List Orders:
GET /api/orders/
- Create Order:
POST /api/orders/
- Retrieve Order:
GET /api/orders/{id}/
- Update Order Status (Admin only):
PATCH /api/orders/{id}/
- Delete Order: Not allowed.
- List Products:
GET /api/products/
- Create Product:
POST /api/products/
(Admin only) - Retrieve Product:
GET /api/products/{id}/
- Update Product:
PUT /api/products/{id}/
(Admin only) - Bulk Upload Products:
POST /api/products/bulk-upload/
(Admin only)
See detailed explanation below.
- URL:
/api/products/bulk-upload/
- Method:
POST
- Description: Allows admins to upload multiple products at once using a CSV file.
POST /api/products/bulk-upload/
Content-Type: multipart/form-data
File: products.csv
name,stock,price,category
Product1,10,100,Category1
Product2,20,200,Category2
{
"products_created": 2,
"errors": []
}
- Only CSV files are supported for now.
- Each row in the CSV represents a product with fields:
name
,stock
,price
,category
. - If there are errors, the response will indicate which rows failed.
- List Categories:
GET /api/categories/
- Calculate Average Price (up to Three Levels):
GET /api/categories/<id>/calculate_average_price/
See detailed explanation below.
- URL:
/api/categories/<id>/calculate_average_price/
- Method:
GET
- Description: Calculates and returns the average price of products for the selected category, including products in immediate subcategories and nested subcategories up to three levels deep.
(
{
"category": Electronics,
"average_price": 500,
"products_count": 3,
"subcategory_count": 2,
},
status=status.HTTP_200_OK,
)
- The calculation includes main category, subcategories, and their immediate children (i.e., up to three levels deep).
- Why only three levels? Aiming for a balance between performance and relevance:
- Two levels deep captures enough data for accurate calculations without including deeply nested categories that might distort the results.
- Ensuring that our queries remain performant and scalable while providing accurate insights for related categories.
- Going beyond three levels introduces data from potentially unrelated or niche subcategories with significantly different pricing, which can skew the average.
Electronics
└── Laptops (level 1)
├── Gaming Laptops (level 2)
│ └── High-End Gaming Laptops (level 3) ← Included in the calculation
└── Ultrabooks (level 2)
└── Lightweight Ultrabooks (level 3) ← Included in the calculation
Categories beyond level 3 (e.g., Premium High-End Gaming Laptops
) will not be included in the average calculation.
- This endpoint calculates the average price per subcategory.
- Useful for tracking pricing trends and insights at a more specific level.
- Support for JSON file uploads in bulk product upload.
- Integrate background tasks for periodic category-level price analysis.
This application enforces strict role-based access control using custom permissions and authentication via OpenID Connect (OIDC), with Google as the provider. The following section explains how authentication is handled and how permissions are enforced for different endpoints.
The application uses the mozilla-django-oidc library for OIDC authentication. This enables users to authenticate via Google. All endpoints are secured with the IsAuthenticated
permission, ensuring that only authenticated users can access the system beyond basic read-only actions (like viewing products).
Customers are authenticated using OpenID Connect (OIDC) for a seamless and secure login experience. The flow ensures that customer profiles are complete before accessing protected resources.
-
Login with OIDC:
Customers are redirected to the OIDC provider for authentication. -
Callback and Token Handling:
Upon successful login, the system processes the returned OIDC tokens to authenticate the user. -
Profile Validation:
- After authentication, the system checks if the customer's profile contains a valid phone number.
- If the phone number is missing, the user is redirected to the Update Profile page to complete their details before proceeding. The phone number is important for enabling sms notifications for customers.
-
Access Granted:
Once the profile is complete, the customer can access the application’s full set of features.
Admins use the default Django Admin authentication for login and management tasks. This ensures that admins have full access to manage the system securely.
- Admins can log in through the
/admin
endpoint. - Permissions are set up to restrict access to sensitive resources based on the admin's role.
- The admin panel provides full control over categories, products, orders, and user management.
The following authentication classes are configured in settings.py
:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'mozilla_django_oidc.contrib.drf.OIDCAuthentication', # Core OIDC Authentication
'rest_framework.authentication.SessionAuthentication', # for web-based sessions
),
}
- OIDCAuthentication: Verifies user identity via Google OpenID Connect.
- SessionAuthentication: Supports browser-based sessions for authenticated users.
Permissions are managed using custom classes to ensure appropriate access based on user roles. Below is a breakdown of permissions for each resource:
- Customers:
- Can create new orders.
- Can view only their own orders.
- Cannot update or delete any order.
- Admins:
- Can view all orders.
- Can update the status of an order using the
PATCH
method. - Can approve and cancel orders.
- Cannot delete or modify other order details.
- Customers: Read-only access to product information.
- Admins: Full access to create, update, and delete products. NB: can only edit product details like price and stock
For Admins only
- Can create and edit categories.
I have implemented custom permission classes to enforce fine-grained access control. Here’s an overview of the key permission rules:
-
IsAdminOrReadOnly
- Read-Only Access: Available to all authenticated users.
- Modification Access: Restricted to admin users.
-
IsOrderOwnerOrAdminWithLimitedUpdate
- Customers: Can create and only access their own orders.
- Admins: Can view all orders and update the status using the
PATCH
method, but cannot delete or full modify order details(PUT
operations). - Used for managing orders.
```python
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsAdminOrReadOnly(BasePermission):
"""Allow read-only access for everyone, but only admins can modify resources."""
def has_permission(self, request, view):
# Allow read-only access for everyone
if request.method in SAFE_METHODS:
return True
# Only authenticated admins can modify resources
return (
request.user.is_authenticated
and hasattr(request.user, "role")
and request.user.role == User.ADMIN
)
def has_object_permission(self, request, view, obj):
# Ensure the user is authenticated and has a role attribute before checking
if (
request.user.is_authenticated
and hasattr(request.user, "role")
and request.user.role == User.ADMIN
):
return True
# Deny all other modifications
return False
```
- The custom permission classes are applied in the corresponding viewsets.
Example for Orders:
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.select_related('customer').prefetch_related('order_items__product').all()
serializer_class = OrderSerializer
permission_classes = [IsAuthenticated, IsOrderOwnerOrAdminWithLimitedUpdate]
Example for Products:
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [IsAuthenticated, IsAdminOrReadOnly]
- GET /products/ – Any authenticated user can view the product list.
- PATCH /products/{id}/ – Only admin users can modify product details (e.g., price, stock).
- DELETE /products/{id}/ – Only admin users can delete a product.
- GET /orders/ – Admin users can view all orders, while customers can only view their own orders.
- PATCH /orders/{id}/ – Admin users can modify order status; customers cannot modify any order.
- Authenticated Customer Viewing Products: Allowed.
- Customer Attempting to Modify a Product: Denied (
403 Forbidden
). - Admin Modifying Product Price and Stock: Allowed (
200 OK
). - Unauthenticated User Accessing Any Endpoint (Except Login): Denied (
401 Unauthorized
).
The following tests ensure that permissions are correctly enforced:
- Admin Access: Ensure admins can modify and delete products.
- Customer Restrictions: Ensure customers can only view products and orders they own.
- Unauthenticated Access: Ensure unauthenticated users are denied access.
Example Test for Admin Access:
@pytest.mark.django_db
def test_admin_can_update_product(user_admin, product_factory):
client = APIClient()
product = product_factory()
client.force_authenticate(user=user_admin)
response = client.patch(
reverse('product-detail', args=[product.id]),
{'price': 200, 'stock': 50},
format='json'
)
assert response.status_code == status.HTTP_200_OK
product.refresh_from_db()
assert product.price == 200
assert product.stock == 50
This section covers all relevant aspects of authentication and permission management. For detailed endpoint descriptions and additional examples, see the API Endpoints section.
The project uses GitHub Actions for continuous integration and deployment. The workflow includes:
- Building the Docker image: Ensures the application can be containerized without errors.
- Running Tests: Executes
pytest
to validate the code. - Pushing to Docker Hub: Builds and pushes the Docker image to Docker Hub.
- Deployment to Kubernetes: Deploys the latest image to the Kubernetes cluster. An example CD job is defined as deployment was done local environment using
minikube