Skip to content

Repository files navigation

Secure Document Verification API

A learning-focused Django REST Framework backend for uploading, reviewing, approving, rejecting, and publicly verifying documents.

This project is designed to teach real backend engineering patterns, not just basic CRUD. The goal is to build a small but realistic API like something used by schools, recruitment agencies, government offices, or companies that need to verify whether submitted documents are genuine.

Problem

Organizations often receive documents such as CVs, certificates, transcripts, licenses, IDs, and contracts through manual channels like email. These documents can be forged, altered, duplicated, hard to track, or difficult to verify later.

Solution

The API will allow users to upload documents, officers to review them, and public visitors to verify approved documents using a unique verification code.

Document uploaded
-> Pending verification
-> Officer reviews
-> Approved or rejected
-> Public verification link becomes usable

Example public verification URL:

https://verify.company.com/doc/ABC123XYZ

Current Status

Phase 3 authentication is complete.

Current files:

requirements.txt
README.md
manage.py
config/
accounts/
authentication/
documents/
audits/

Installed packages:

Django 5.2.15
Django REST Framework 3.17.1
djangorestframework-simplejwt 5.5.1
drf-spectacular 0.29.0
psycopg 3.3.4
dj-database-url 3.1.2
python-decouple 3.8

Phase 4 document management is complete. The project is now being moved from SQLite to PostgreSQL for local development.

Fresh Clone Setup

Use this path when setting up the project on a new machine.

Prerequisites

Python 3.12+
PostgreSQL
Git
make

1. Clone the Project

git clone <repo-url>
cd secure-docs-api

2. Create and Activate Virtual Environment

python3 -m venv venv
source venv/bin/activate

3. Install Dependencies

pip install -r requirements.txt

4. Create Local Environment File

cp .env.example .env

Update at least these values:

SECRET_KEY=change-this-local-secret-key
JWT_SIGNING_KEY=change-this-to-a-long-random-jwt-signing-key
DATABASE_NAME=secure_docs_db_local
DATABASE_USER=postgres
DATABASE_PASSWORD=

5. Create PostgreSQL Database

If your local PostgreSQL user can create databases:

createdb secure_docs_db_local

Or from psql:

CREATE DATABASE secure_docs_db_local;

6. Apply Migrations

make migrate

7. Create Admin User

make createsuperuser

8. Verify the Project

make verify

9. Start the Server

make run

Open Swagger:

http://localhost:4000/api/docs/

Daily Developer Commands

make run
make test
make test-app APP=documents
make makemigrations
make migrate
make schema
make verify

Common First-Run Issues

PostgreSQL is not running        -> start PostgreSQL, then run make migrate
database does not exist          -> create secure_docs_db_local
missing .env                     -> cp .env.example .env
port 4000 already in use         -> make run PORT=8000
unapplied migrations             -> make migrate
dependencies missing             -> activate venv and run pip install -r requirements.txt

Learning Goals

This project will cover:

  • Authentication
  • Authorization
  • PostgreSQL
  • File uploads
  • Role-based access control
  • Audit logs
  • Email notifications
  • Search and filtering
  • Pagination
  • Docker
  • Testing
  • Deployment
  • Production security

Learning Guide for Spring Boot Developers

If you are coming from Spring Boot, read the dedicated DRF guide:

docs/spring-boot-to-drf-guide.md

It explains this project from start to end by mapping Spring Boot concepts like Controller, Entity, Repository, DTO, Spring Security, ResponseEntity, ControllerAdvice, Flyway, and Swagger to their Django REST Framework equivalents.

System Users

Admin

Can manage users, manage officers, view all documents, and view audit logs.

Verification Officer

Can review documents, approve documents, reject documents, and add review remarks.

User

Can register, upload documents, view document status, and download their own documents.

Public Visitor

Can verify document authenticity without logging in.

Main Features

Authentication

Implemented endpoints:

POST /api/auth/register/
POST /api/auth/login/
POST /api/auth/refresh/
POST /api/auth/logout/
GET  /api/accounts/profile/

Refresh request:

POST /api/auth/refresh/
Authorization: Bearer <refresh_token>

In Swagger, use the normal Authorize button for this endpoint, but paste the refresh token instead of the access token.

JWT behavior:

Access tokens expire quickly.
Refresh tokens last longer and rotate on refresh.
Refresh endpoint reads the refresh token from Authorization: Bearer <refresh_token>.
Old refresh tokens are blacklisted after rotation.
Logout blacklists the submitted refresh token.
JWT_SIGNING_KEY is read from the environment.
Protected endpoints validate the token and load the current user from the database.
Deleted, inactive, or invalid-role users cannot use old access tokens.
Authorization decisions use the database user role, not a role claim from the token.
Account-state authentication failures return one generic message and log the internal reason server-side.

API Documentation

Swagger/OpenAPI documentation is available through drf-spectacular.

GET /api/schema/
GET /api/docs/

Standard API Response Format

All JSON API responses are wrapped in one consistent envelope, similar to the response DTO style commonly used in NestJS or Spring Boot controllers.

Successful response:

{
  "success": true,
  "message": "Document fetched successfully",
  "data": {
    "id": 1,
    "title": "Degree Certificate",
    "status": "PENDING"
  }
}

Business error response:

{
  "success": false,
  "message": "Only admins and officers can review documents",
  "status": 403,
  "error": "DOCUMENT_REVIEW_FORBIDDEN",
  "data": null
}

Validation error response:

{
  "success": false,
  "message": "email: This field is required.",
  "status": 400,
  "error": null,
  "data": null,
  "errors": {
    "email": ["This field is required."]
  }
}

In this project:

common/renderers.py    -> wraps normal successful API responses
common/exceptions.py   -> wraps validation, permission, auth, and not-found errors
view.response_message  -> sets the success message for a simple view
view.response_messages -> sets success messages per ViewSet action

Document Management

Implemented endpoints:

POST   /api/documents/
GET    /api/documents/
GET    /api/documents/{id}/
PATCH  /api/documents/{id}/
DELETE /api/documents/{id}/

Document create/update endpoints accept multipart/form-data so files can be uploaded from Swagger. Document edits use PATCH; full PUT replacement is disabled.

Form-data fields:

title
file
description

Verification Workflow

Document statuses:

PENDING
UNDER_REVIEW
APPROVED
REJECTED

Officer review endpoint:

POST /api/documents/{id}/review/

Request examples:

{
  "action": "START_REVIEW",
  "review_notes": "Initial review started."
}
{
  "action": "APPROVE",
  "review_notes": "Document details match official records."
}
{
  "action": "REJECT",
  "review_notes": "Certificate number could not be verified."
}

Review metadata stored on each document:

reviewed_by
reviewed_at
review_notes

Public Verification

Implemented endpoint:

GET /api/verify/{verification_code}/

Example response:

{
  "success": true,
  "message": "Document verified successfully",
  "data": {
    "verified": true,
    "title": "Degree Certificate",
    "document_type": "Certificate",
    "verification_code": "AB12CD34E",
    "status": "APPROVED",
    "reviewed_at": "2026-06-16T10:30:00Z"
  }
}

Public verification does not expose file URLs, user emails, reviewer emails, review notes, or internal user ids.

Audit Logging

Actions to track:

DOCUMENT_UPLOADED
DOCUMENT_REVIEW_STARTED
DOCUMENT_APPROVED
DOCUMENT_REJECTED
DOCUMENT_DELETED
DOCUMENT_VERIFICATION_SUCCEEDED
DOCUMENT_VERIFICATION_FAILED

Planned Database Design

User

id
email
first_name
last_name
role
is_active
created_at

Document

id
title
file
description
verification_code
status
uploaded_by
created_at
updated_at

VerificationReview

id
document
officer
decision
remarks
reviewed_at

AuditLog

id
user
action
entity
entity_id
metadata
created_at

Spring Boot to DRF Mental Model

Spring Boot Django REST Framework
Entity Model
Repository ORM Manager
DTO Serializer
Controller ViewSet or APIView
Spring Security DRF Permissions
JWT Filter SimpleJWT
Swagger / springdoc-openapi drf-spectacular
application.properties settings.py and .env
JPA Django ORM
Flyway Migrations

Learning Roadmap

Phase 1: Environment Setup

  • Python
  • Virtual environment
  • Django
  • Django REST Framework
  • Git
  • PostgreSQL

Phase 2: Project Architecture

  • Django project creation
  • App structure
  • Custom user model
  • Environment variables
  • Settings separation
  • Completed

Phase 3: Authentication System

  • JWT
  • Registration
  • Login
  • Refresh tokens
  • Profile endpoint
  • Permissions
  • Completed

Phase 4: Document Module

  • Models
  • Serializers
  • ViewSets
  • File uploads
  • Validation
  • Completed

Phase 5: Verification Workflow

  • Approval process
  • Rejection process
  • Status transitions
  • Business rules
  • Audit events for document upload, review, approval, rejection, and deletion
  • Completed

Phase 6: Public Verification API

  • Verification codes
  • Public endpoint
  • Safe public response shape
  • Audit logs for public verification attempts
  • Completed

Phase 7: Audit Logging

  • Audit model
  • Admin visibility
  • Optional admin-only audit API
  • Optional middleware for request-level activity tracking
  • Activity tracking

Phase 8: Testing

  • Unit tests
  • API tests
  • Permission tests
  • Workflow tests

Phase 9: Docker

  • Dockerfile
  • Docker Compose
  • PostgreSQL container
  • Local development environment

Phase 10: Production Deployment

  • Gunicorn
  • Nginx
  • Render or Railway
  • CI/CD
  • Production security checks

Phase 2 Step-by-Step Plan: Project Architecture

Phase 2 is about setting up the shape of the project before building features. A good Django REST Framework project becomes much easier to understand when each app has a clear responsibility.

Step 1: Confirm the Environment

Make sure the virtual environment is active and Django is available.

source venv/bin/activate
python --version
django-admin --version
pip show djangorestframework

Success means:

Python 3.12.x
Django 5.2.15
djangorestframework installed

Step 2: Create the Django Project

Create the main Django project in the current folder.

django-admin startproject config .

Why config?

config/ will hold project-level settings, URLs, ASGI, and WSGI files.
Feature code will live in separate apps.

Expected structure:

manage.py
config/
  __init__.py
  asgi.py
  settings.py
  urls.py
  wsgi.py

Step 3: Create the Core Apps

Create separate apps for the main business areas.

python manage.py startapp accounts
python manage.py startapp authentication
python manage.py startapp documents
python manage.py startapp audits

App responsibilities:

authentication/ -> register, login, refresh-token workflows
accounts/       -> users, roles, profile, permissions
documents/      -> uploads, document metadata, verification workflow
audits/         -> audit trail and activity history

Step 4: Register Apps in Settings

Add DRF and the local apps to INSTALLED_APPS inside config/settings.py.

Planned apps:

rest_framework
accounts
documents
audits

Success means Django can detect all project apps without errors.

python manage.py check

Step 5: Decide the Custom User Model Early

Django projects should define a custom user model at the beginning, before the first migration.

Planned user direction:

Use email as the login field.
Add a role field for ADMIN, OFFICER, and USER.
Keep is_active for account control.

Why this matters:

Changing the user model later is painful after migrations and data exist.

The actual model implementation belongs to Phase 3, but Phase 2 must prepare for it.

Step 6: Plan Settings Separation

For now, keep config/settings.py simple. Later, split settings when the project grows.

Future structure:

config/settings/
  __init__.py
  base.py
  local.py
  production.py

Do not split too early unless the project needs it. First learn the normal Django settings file, then refactor with understanding.

Step 7: Plan Environment Variables

Create an environment strategy before production settings.

Variables this project will eventually need:

SECRET_KEY
DEBUG
ALLOWED_HOSTS
DATABASE_URL
EMAIL_HOST
EMAIL_PORT
EMAIL_HOST_USER
EMAIL_HOST_PASSWORD

Recommended future package:

python-decouple

This can wait until database and deployment work begin.

Step 8: Plan URL Structure

Project-level URLs should route into app-level URLs.

Target structure:

config/urls.py
authentication/urls.py
accounts/urls.py
documents/urls.py
audits/urls.py

Planned URL prefixes:

/api/auth/
/api/accounts/
/api/documents/
/api/verify/
/api/audits/

Step 9: Plan Media File Handling

Documents will require file uploads, so the architecture needs media settings.

Planned local settings:

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

Later, production should use external storage instead of keeping uploaded files only on the server disk.

Step 10: Run the First Architecture Check

After creating the project and apps, run:

python manage.py check
python manage.py runserver

Success means:

Django starts without errors.
The local server opens.
The app structure is ready for custom user work.

Do not run the first database migration until the custom user model is created in Phase 3.

Phase 2 Completion Checklist

  • Django project created as config
  • manage.py exists
  • accounts, documents, and audits apps exist
  • Apps are registered in INSTALLED_APPS
  • python manage.py check passes
  • Initial migrations were deferred until the custom user model existed
  • URL strategy is clear
  • Media upload strategy is clear
  • Custom user model direction is decided before Phase 3

Phase 3 Step-by-Step Plan: Authentication System

Phase 3 turns the project into a real API with users, roles, and token-based authentication. The most important rule is to create the custom user model before running the first migration.

Step 1: Confirm Phase 2 Is Clean

Run the Django system check before editing authentication code.

source venv/bin/activate
python manage.py check

Success means:

System check identified no issues

Step 2: Install JWT Support

Add SimpleJWT for access and refresh tokens.

pip install djangorestframework-simplejwt
pip freeze > requirements.txt

Why:

DRF handles API structure.
SimpleJWT handles token creation, refresh, and authentication.

Step 3: Create the Custom User Model

Update accounts/models.py with a custom user model.

Planned fields:

email
first_name
last_name
role
is_active
is_staff
date_joined

Planned roles:

ADMIN
OFFICER
USER

Design decision:

email will be the login field.
username will not be used.

Step 4: Create the User Manager

Create a custom manager for user creation.

Required methods:

create_user(email, password, **extra_fields)
create_superuser(email, password, **extra_fields)

Why:

Django needs a manager that knows how to create normal users and admin users when email replaces username.

Step 5: Register the Custom User in Settings

Add this to config/settings.py:

AUTH_USER_MODEL = "accounts.User"

This must happen before the first migration.

Step 6: Configure DRF Authentication

Add REST framework authentication settings in config/settings.py.

Planned configuration:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ),
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",
    ),
}

Why:

Most API endpoints should require login by default.
Public endpoints will explicitly allow anonymous access later.

Step 7: Create Authentication and Account Serializers

Create authentication/serializers.py and accounts/serializers.py.

Planned serializers:

authentication.RegisterSerializer -> validate registration input and create users
accounts.UserSerializer           -> return safe profile data

Do not return password hashes in API responses.

Step 8: Create Authentication and Account Views

Update authentication/views.py and accounts/views.py.

Planned views:

authentication.RegisterView
authentication.LoginView
authentication.RefreshTokenView
accounts.ProfileView

Login inherits from SimpleJWT. Refresh uses a custom view so the refresh token can be read from the Bearer header.

Step 9: Wire Authentication URLs

Update authentication/urls.py and accounts/urls.py.

Planned endpoints:

POST /api/auth/register/
POST /api/auth/login/
POST /api/auth/refresh/
GET  /api/accounts/profile/

Mapping:

authentication/register/ -> custom RegisterView
authentication/login/    -> SimpleJWT LoginView
authentication/refresh/  -> custom RefreshTokenView with SimpleJWT refresh serializer
accounts/profile/        -> custom ProfileView

Step 10: Create Role-Based Permissions

Create accounts/permissions.py.

Planned permissions:

IsAdmin
IsOfficer
IsDocumentOwner

These permissions will be used heavily in later phases when documents and verification workflows are built.

Step 11: Make and Run the First Migrations

Only run migrations after the custom user model and AUTH_USER_MODEL are in place.

python manage.py makemigrations
python manage.py migrate

Success means:

accounts migration is created.
Database tables are created.
No custom user model errors appear.

Step 12: Create a Superuser

Create the first admin user.

python manage.py createsuperuser

Expected login field:

Email

If Django asks for Username, the custom user setup is not correct yet.

Step 13: Test the Authentication Flow Manually

Run the server.

python manage.py runserver

Manual checks:

Register a user.
Log in and receive access plus refresh tokens.
Use the access token to call profile.
Refresh the token successfully.
Confirm the refresh response returns a new refresh token.
Log out and confirm the refresh token cannot be reused.

Step 14: Add Authentication Tests

Update authentication/tests.py and accounts/tests.py.

Minimum tests:

User can register.
User can log in.
Refresh token rotation returns a new refresh token.
Old refresh token cannot be reused after rotation.
Authenticated user can log out.
Logged-out refresh token cannot be reused.
Anonymous user cannot log out.
Authenticated user can view profile.
Anonymous user cannot view profile.
Superuser is created with ADMIN role.

Phase 3 Completion Checklist

  • SimpleJWT installed and saved in requirements.txt
  • Custom email-based user model exists
  • Custom user manager exists
  • AUTH_USER_MODEL is configured
  • DRF uses JWT authentication
  • Register endpoint works
  • Login endpoint returns tokens
  • Refresh endpoint returns a new access token and rotated refresh token
  • Old refresh tokens are blacklisted after rotation
  • Logout endpoint blacklists refresh tokens
  • Profile endpoint requires authentication
  • Role permission classes are started
  • First migrations are created and applied
  • Superuser creation uses email
  • Authentication tests pass

Phase 4 Step-by-Step Plan: Document Module

Phase 4 adds the main business object of the system: documents. This phase teaches file uploads, ownership rules, serializers, viewsets, filtering, and API tests.

Step 1: Confirm Authentication Is Healthy

Run the existing checks before adding document code.

source venv/bin/activate
python manage.py check
python manage.py test

Success means:

The authentication tests pass.
Django reports no system check issues.

Step 2: Design the Document Model

Update documents/models.py.

Planned fields:

title
file
description
verification_code
status
uploaded_by
created_at
updated_at

Initial statuses:

PENDING
UNDER_REVIEW
APPROVED
REJECTED

Important model rules:

uploaded_by links to the custom user model.
verification_code must be unique.
new documents start as PENDING.
review fields are set by admins/officers during verification.
file uploads are stored under a documents/ media folder.

Step 3: Generate Verification Codes

Add automatic verification code generation when a document is first created.

Code requirements:

unique
hard to guess
short enough to share
safe for URLs

Example:

ABC123XYZ

Step 4: Create and Run Document Migrations

After the model is ready:

python manage.py makemigrations documents
python manage.py migrate

Success means:

documents migration is created.
Document table exists in the database.
No auth model migration errors appear.

Step 5: Register Document in Django Admin

Update documents/admin.py.

Admin should show useful fields:

title
status
verification_code
uploaded_by
created_at
updated_at

Useful admin filters:

status
created_at
updated_at

Step 6: Create Document Serializers

Create documents/serializers.py.

Planned serializers:

DocumentSerializer
DocumentCreateSerializer

Serializer rules:

uploaded_by is read-only.
verification_code is read-only.
status is read-only for normal users.
file is required on create.

Step 7: Create Document Permissions

Create documents/permissions.py.

Planned permission behavior:

Admins can view all documents.
Officers can view documents for review.
Users can view and manage only their own documents.
Public visitors cannot access document management endpoints.

This builds on the IsAdmin, IsOfficer, and IsDocumentOwner concepts from Phase 3.

Step 8: Create the Document ViewSet

Update documents/views.py.

Use a DRF ModelViewSet for:

list
create
retrieve
partial_update
destroy

Behavior rules:

normal users see only their own documents.
admins and officers can see all documents.
new documents are assigned to request.user.

Step 9: Wire Document URLs

Update documents/urls.py.

Use a DRF router.

Target endpoints:

POST   /api/documents/
GET    /api/documents/
GET    /api/documents/{id}/
PATCH  /api/documents/{id}/
DELETE /api/documents/{id}/

Step 10: Validate File Upload Behavior

Test document upload through:

Swagger UI file picker
DRF APIClient
manual multipart request

Validation rules to consider:

file is required.
title is required.
only authenticated users can upload.
uploaded files are saved under media/documents/.

Step 11: Add Search, Filtering, and Ordering

Add basic query features.

Planned support:

search by title
filter by status
order by created_at

Likely DRF tools:

SearchFilter
OrderingFilter

Step 12: Add Pagination

Configure default pagination for document lists.

Planned behavior:

document lists return paginated results.
page size stays small for development.

This prepares the API for real-world document volumes.

Step 13: Add Document Tests

Update documents/tests.py.

Minimum tests:

authenticated user can upload a document.
anonymous user cannot upload a document.
user can list only their own documents.
admin can list all documents.
document receives a verification code.
document starts as PENDING.
user can retrieve own document.
user cannot retrieve another user's document.

Step 14: Verify Swagger Documentation

After the document endpoints are wired, confirm Swagger shows file upload fields.

python manage.py spectacular --file /tmp/secure-docs-schema.yml
python manage.py runserver

Open:

http://127.0.0.1:8000/api/docs/

Expected Swagger behavior:

POST /api/documents/ uses multipart/form-data.
file is shown as a binary upload field.
Swagger UI shows a file picker.

Phase 4 Completion Checklist

  • Document model exists
  • verification code generation works
  • document migration is created and applied
  • document appears in Django admin
  • document serializers exist
  • ownership permissions exist
  • document viewset exists
  • document URLs are wired
  • authenticated users can upload files
  • users can only manage their own documents
  • admins and officers can review all documents
  • list endpoint supports search, filtering, ordering, and pagination
  • document tests pass
  • Swagger shows document endpoints with a file picker for uploads

API Documentation

Run the development server:

python manage.py runserver

Open Swagger UI:

http://127.0.0.1:8000/api/docs/

Open the raw OpenAPI schema:

http://127.0.0.1:8000/api/schema/

Generate the schema from the command line:

python manage.py spectacular --file /tmp/secure-docs-schema.yml

Common Make Commands

The project includes a Makefile for common local commands:

make help
make run
make check
make test
make test-app APP=authentication
make schema
make migration-check
make verify
make migrate
make createsuperuser

By default, make run starts the server on port 4000. You can override it:

make run PORT=8000

PostgreSQL Setup

The project now reads database settings from environment variables.

Create a local .env file from the example:

cp .env.example .env

Recommended local .env shape:

PORT=4000
DEBUG=True
SECRET_KEY=change-this-local-secret-key
ALLOWED_HOSTS=localhost,127.0.0.1,testserver

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=secure_docs_db_local
DATABASE_USER=postgres
DATABASE_PASSWORD=

LOCAL_SERVER=http://localhost:4000
STAGING_SERVER=
PRODUCTION_SERVER=
SARTIFY_SERVER=

If your postgres user has a password, put it in DATABASE_PASSWORD.

You can also use a single URL:

DATABASE_URL=postgresql://postgres:mypassword%40123@localhost:5432/secure_docs_db_local

If a password contains @, write it as %40 inside DATABASE_URL.

Run migrations after PostgreSQL is reachable:

python manage.py migrate
python manage.py createsuperuser

Run the development server on the same style of port you use in other projects:

python manage.py runserver 0.0.0.0:4000

Local Setup

Activate the virtual environment:

source venv/bin/activate

Check installed versions:

python --version
django-admin --version
pip show djangorestframework
pip show djangorestframework-simplejwt
pip show drf-spectacular
pip show psycopg
pip show dj-database-url
pip show python-decouple

Install dependencies from requirements.txt if needed:

pip install -r requirements.txt

Next Step

Finish the local PostgreSQL switch:

Create or update .env
Confirm PostgreSQL is running
Run migrations on secure_docs_db_local
Create a new PostgreSQL-backed superuser

After that, the next major milestone will be the verification workflow.

About

Django REST Framework API for secure document upload, review, approval, audit logging, and public verification.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages