Skip to content

Latest commit

 

History

History
453 lines (368 loc) · 22.9 KB

File metadata and controls

453 lines (368 loc) · 22.9 KB

Placement Portal Engineering Guide

This guide records the behavior of the current implementation. It is intended to shorten onboarding time and make the impact of changes easier to assess.

System at a glance

The application is a three-role placement workflow:

  • Administrators approve companies and drives, manage account access, inspect applications, and download generated reports.
  • Companies publish drives, control whether they are open, review applicants, advance applications, upload offer letters, and export application data.
  • Students maintain academic profiles and resumes, browse eligible drives, apply, track decisions, download offer letters, and export their history.

The repository is a single deployment unit with a Vue single-page application, a Flask JSON API, one SQLite database, Redis, Celery, SMTP, and local file storage.

Browser
  |
  +-- Vue/Vite :5173
          |
          +-- /api proxy --> Flask :5001
                                |
                                +-- SQLite
                                +-- Redis DB 1 (HTTP response cache)
                                +-- Redis DB 0 (Celery broker and results)
                                +-- local uploads, exports, and reports
                                +-- SMTP through Flask-Mail

Celery worker <---- Redis DB 0 ----> Celery Beat

Repository map

Path Responsibility
backend/app.py Application factory, configuration, JWT callbacks, cache invalidation, static SPA fallback, and all HTTP endpoints
backend/eligibility.py Shared CGPA, branch-alias, and study-year eligibility rules
backend/models.py SQLAlchemy schema, relationships, constraints, table creation, and development seed users
backend/tasks.py Student/company CSV exports, deadline reminders, and monthly report generation
backend/celery_app.py Celery construction, Flask application context integration, result retention, timezone, and Beat schedule
backend/extensions.py Shared Flask-Caching and Flask-Mail instances
backend/email_helper.py HTML email delivery and ADMIN_EMAIL fallback
backend/config.py Local defaults loaded before environment-specific overrides
backend/wsgi.py Gunicorn entry point for container and production runs
backend/seed_demo.py Optional demo dataset for local demos and screenshots; refuses to run twice
frontend/src/App.vue Application shell, navigation, and logout
frontend/src/router/index.js Public and role-specific routes plus browser-side route guards
frontend/src/utils/api.js Axios base URL, bearer-token attachment, and 401 handling
frontend/src/utils/auth.js Login, registration, and local-storage session state
frontend/src/views/ Admin, company, student, login, registration, and home pages
frontend/src/components/ Shared interface pieces: data table, segmented tabs, status badges, stat cards, empty states, toasts, and the confirmation dialog
frontend/src/components/AppBackground.vue The single full-window backdrop instance mounted by the shell
frontend/src/components/BeamsBackground.vue Lifecycle wrapper for the effect: lazy load, motion preference, visibility pausing, disposal
frontend/src/components/CountUp.vue Numbers that roll up once scrolled into view
frontend/src/components/SpotlightCard.vue Cursor-tracking highlight applied through custom properties
frontend/src/components/ShinyText.vue Sweeping sheen used for accent copy
frontend/tests/ Vitest component, service, router, and lifecycle suites
frontend/src/components/beams.js Framework-free WebGL beams: noise-displaced standard material over one merged geometry
frontend/src/assets/theme.css Design tokens and Bootstrap overrides for the light and dark themes, including the shared glass material
backend/tests/support.py Shared test fixtures: throwaway database, file tree, and registration helpers
backend/tests/ Workflow, authorization, file-access, configuration, and eligibility suites
.github/workflows/ci.yml Continuous integration: tests, coverage gate, boundaries, secret scan, lint, build, container
Dockerfile Three-stage build producing one image for the API, worker, and Beat
docker-compose.yml Local production-shaped stack with Redis and named volumes
scripts/api-smoke.sh Curl smoke test against a running backend: authentication, role boundaries, and the company approval gate
run.sh Starts Flask, Vite, a Celery worker, and Celery Beat as child processes

Runtime initialization

backend/app.py:create_app() performs more than object construction:

  1. Discovers and loads .env through python-dotenv; with the documented launch commands this is normally backend/.env.
  2. Resolves Flask, JWT, database, Redis, cache, and mail configuration.
  3. Creates the resume, offer-letter, export, and report directories.
  4. Initializes SQLAlchemy.
  5. Calls db.create_all().
  6. Seeds the admin, approved test company, and test student if absent.
  7. Initializes Redis-backed caching and Flask-Mail.
  8. Registers the routes defined inside the factory.

Importing backend/celery_app.py also creates a Flask application, then wraps every Celery task in that application's context. Worker and Beat startup therefore run database initialization and seed checks as import side effects.

The relative default database URL, sqlite:///ppa_db.db, is resolved by Flask-SQLAlchemy under Flask's instance directory. With the current layout this is normally backend/instance/ppa_db.db.

Configuration

Variable Default Used by
APP_ENV development Selects fail-closed secret handling
FLASK_SECRET_KEY Random per process outside production Flask session signing
JWT_SECRET_KEY Random per process outside production JWT signing
JWT_ACCESS_TOKEN_MINUTES 60 Access-token lifetime
CORS_ORIGINS Three local development origins Allowed browser origins
DATABASE_URL sqlite:///ppa_db.db SQLAlchemy
CELERY_BROKER_URL redis://localhost:6379/0 Celery transport
CELERY_RESULT_BACKEND redis://localhost:6379/0 Celery task results
REDIS_HOST localhost Flask-Caching
REDIS_PORT 6379 Flask-Caching
MAIL_SERVER smtp.gmail.com Flask-Mail
MAIL_PORT 587 Flask-Mail
MAIL_USE_TLS true Flask-Mail
MAIL_USERNAME Empty SMTP authentication and mail suppression
MAIL_PASSWORD Empty SMTP authentication
MAIL_DEFAULT_SENDER MAIL_USERNAME Outgoing sender
ADMIN_EMAIL Empty Default recipient for monthly reports
MAX_UPLOAD_BYTES 5242880 Maximum HTTP upload size
FLASK_DEBUG false Development debugger
PORT 5001 Flask listen port

Redis database 0 is shared by the broker and result backend. Redis database 1 holds cached HTTP responses only; export ownership lives in the database.

APP_ENV=production requires FLASK_SECRET_KEY and JWT_SECRET_KEY to be present and at least 32 characters; start-up raises otherwise. Any other value generates a random secret per process and logs a warning, so no shared default exists in the source tree. CORS_ORIGINS is a comma-separated list; the Vite proxy targets http://localhost:5001.

Interface material

Everything that floats over the animated backdrop draws from one set of glass tokens in theme.css rather than styling itself: --glass-bg, --glass-bg-strong, --glass-edge, --glass-shadow, and --glass-specular, over backdrop-filter: saturate(180%) blur(22px). The navigation capsule, the cards, the home hero, chips, toasts, and the confirmation dialog all use them, so a change to the material propagates everywhere at once. A @supports guard falls back to the opaque surface colour where backdrop-filter is unavailable, because a translucent pane without the blur is unreadable.

The backdrop itself is theme-aware through --app-beams-opacity, --app-beams-blend, and --app-beams-tint. The beam field is bright streaks on black: the dark appearance composites it normally, while the light one screens it over a faintly tinted canvas, which keeps the highlights, discards the shadows, and still leaves the effect visible.

Domain model

Entity Important fields Relationships and constraints
User email, password hash, role, active flag One optional student profile or company profile; email is unique
StudentProfile name, roll number, branch, year, CGPA, resume URL Belongs to one user; roll number is unique
CompanyProfile name, description, point of contact, website, approval status Belongs to one user; owns drives and placement records
PlacementDrive role, package, minimum CGPA, deadline, skills, eligibility, approval status, open/closed status Belongs to one company; owns applications
Application student, drive, status, feedback, interview details, offer-letter URL One record per student and drive, enforced by a database unique constraint
Placement student, company, position, salary, joining date Created when an application is marked PLACED
ExportTask Celery task identifier, requesting user, export kind Written when an export is queued; the authority for task result access

State vocabularies

  • User roles: ADMIN, COMPANY, STUDENT
  • Company approval: PENDING, APPROVED, REJECTED
  • Drive approval: PENDING, APPROVED, REJECTED
  • Drive availability: ACTIVE, CLOSED
  • Application status: APPLIED, SHORTLISTED, INTERVIEW, OFFER, SELECTED, PLACED, REJECTED

The company dashboard presents the main application path as:

APPLIED -> SHORTLISTED -> INTERVIEW -> OFFER -> SELECTED -> PLACED
   |            |             |          |
   +------------+-------------+----------+----> REJECTED

The API validates both the destination status and the transition from the current status. Invalid transitions return HTTP 409.

Core workflows

Company onboarding

  1. Registration creates an active User and a PENDING CompanyProfile.
  2. Login is rejected until the profile is APPROVED.
  3. An administrator can approve the company, deactivate its user, or remove the company and its related applications, placements, drives, profile, and user.

There is no administrator action that changes a company profile to REJECTED; the current UI supports approval, account deactivation, and removal.

Drive lifecycle

  1. An approved company creates an ACTIVE, PENDING drive.
  2. An administrator approves or rejects it.
  3. Students can see only APPROVED, ACTIVE, non-expired drives.
  4. The company can close or reopen its own drive.
  5. Administrators and owning companies can delete a drive and its applications.

Student eligibility checks CGPA, comma-separated branch names, and optional minimum/maximum study year. The shared implementation accepts common branch abbreviations such as CS, CSE, and ECE, and is used by drive listing, application submission, and reminder generation.

Student application

  1. A student needs a resume URL before drive browsing is allowed.
  2. The drive list returns all currently visible drives and an is_eligible value for each.
  3. Submission repeats the approval, availability, deadline, CGPA, branch, year, resume, and duplicate-application checks.
  4. A company advances or rejects the application and can attach feedback, interview details, and an offer-letter PDF.
  5. Marking an application PLACED creates a Placement record. The route checks that no placement already exists for the student.

Exports

Student and company dashboards enqueue a Celery export and poll the common task status endpoint every two seconds. A completed result contains the generated filename and record count. The authenticated download endpoint serves the CSV from backend/exports/.

Task results expire from Redis after one hour. Ownership is an ExportTask row written when the job is queued, so it survives cache eviction and result expiry. Generated CSV files do not currently have a cleanup job.

Reports and reminders

The effective schedule is defined in backend/celery_app.py with the Asia/Kolkata timezone:

Job Effective schedule Behavior
send_daily_reminders Daily at 09:00 Emails each resume-bearing student about eligible drives closing within 48 hours
generate_monthly_admin_report First day at 08:00 Writes an HTML summary under backend/reports/ and emails it to ADMIN_EMAIL

The report aggregates drives, applications, and placements created since the start of the current month and lists the five most-applied drives.

API inventory

All protected routes require an Authorization: Bearer <token> header. Role claims are checked again in the backend; browser route guards are not the security boundary. Every request also resolves the token subject: tokens issued to accounts that have since been deactivated or deleted are rejected with 401.

Authentication and service

Method and path Purpose
POST /api/auth/register Register a student or company
POST /api/auth/login Validate credentials and issue an access token
GET /api/health Unauthenticated liveness probe with a database check

Administration

Method and path Purpose
GET /api/admin/stats Aggregate platform counts
GET /api/admin/companies Search and list companies
GET /api/admin/students Search and list students
POST /api/admin/users/:id/toggle-active Activate or deactivate a non-admin user
POST /api/admin/companies/:id/approve Approve a company
DELETE /api/admin/companies/:id/remove Delete a company and related records
GET /api/admin/drives Search and list drives
POST /api/admin/drives/:id/approve Approve a drive
POST /api/admin/drives/:id/reject Reject a drive
DELETE /api/admin/drives/:id/remove Delete a drive and its applications
GET /api/admin/applications Filter applications by status, company, student, or position
GET /api/admin/reports List generated HTML reports
GET /api/admin/reports/:filename View or download a report

Companies

Method and path Purpose
GET /api/company/profile Read the authenticated company profile
GET /api/company/drives List the company's drives
POST /api/company/drives Create a drive
PATCH /api/company/drives/:id/status Close or reopen an owned drive
DELETE /api/company/drives/:id/remove Delete an owned drive and its applications
GET /api/company/applications List applications for company drives
PATCH /api/company/applications/:id Update status, feedback, or interview data
POST /api/company/applications/:id/offer-letter Attach an offer-letter PDF
POST /api/company/export-applications Enqueue a company CSV export

Students

Method and path Purpose
GET /api/student/profile Read the authenticated student profile
PATCH /api/student/profile Update name, branch, year, CGPA, or resume URL
POST /api/student/resume/upload Upload a resume PDF
GET /api/student/drives Search visible drives and calculate eligibility
POST /api/student/apply/:driveId Apply to a drive
GET /api/student/applications List the student's applications
GET /api/student/placements List placement history
POST /api/student/export-applications Enqueue a student CSV export

Shared protected resources

Method and path Purpose
GET /api/resumes/:filename Serve an uploaded resume
GET /api/offer-letters/:filename Serve an uploaded offer letter
GET /api/tasks/:taskId Inspect export progress
GET /api/tasks/:taskId/download Download a completed export

Caching

The following GET routes use Redis-backed response caching with a key containing the request path, JWT identity, and query string:

Endpoint TTL
/api/admin/stats 60 seconds
/api/admin/companies 120 seconds
/api/admin/students 120 seconds
/api/admin/drives 120 seconds
/api/student/drives 60 seconds

Write routes invalidate the affected cache families by scanning the relevant Redis key prefixes. Drive approval, rejection, removal, close, and reopen operations invalidate student drive listings immediately.

File storage and access

Directory Contents Retention
backend/uploads/resumes/ Student-uploaded PDFs Indefinite
backend/uploads/offers/ Company-uploaded offer letters Indefinite
backend/exports/ Celery-generated CSV files Indefinite
backend/reports/ Celery-generated monthly HTML reports Indefinite

Filenames are generated from internal IDs and UUIDs. Upload validation checks the .pdf suffix and PDF file signature. Flask rejects requests above the configurable upload limit, which defaults to 5 MiB.

Serving a stored file follows the same three steps everywhere:

  1. Normalize the requested name with secure_filename.
  2. Find the database row that references it — the student profile for a resume, the application for an offer letter, the export-task record for a CSV. A name with no owning row is a 404, so a guessed or crafted filename grants nothing.
  3. Resolve the path through resolve_stored_file(), which confines it to the intended directory and refuses anything that escapes.
Resource Readable by
Resume The owning student, an administrator, and companies the student has an application with
Offer letter The recipient student, the issuing company, and administrators
Monthly report Administrators only, and only files ending in .html
Export CSV The user who queued the export and administrators

Because internal resume paths would otherwise be claimable, user-supplied resume URLs must be absolute http:// or https:// links; the /api/resumes/ namespace is writable only by the upload endpoint. A profile update may resubmit the student's own unchanged resume path, so clients that round-trip the whole profile still work.

Authorization is checked before any task state is disclosed, so the shared task endpoints do not confirm whether an unrelated task exists.

Change-impact guide

  • A schema change touches backend/models.py, serialization in backend/app.py, relevant CSV/report code in backend/tasks.py, and dashboard form/table code. There is no migration mechanism, so db.create_all() will not alter existing tables.
  • A new protected endpoint should use @jwt_required(), enforce a backend role check, scope the database query to the authenticated profile, and invalidate any cached lists it changes. Add it to the role matrix in backend/tests/test_authorization.py, which asserts that every other role is refused.
  • A new endpoint that serves a stored file must locate the owning database row before reading disk and resolve the path through resolve_stored_file(). Never derive authorization from the filename.
  • An application status change affects the model vocabulary, backend transition map, company action controls, student/admin badge mappings, and CSV exports.
  • Eligibility rules belong in backend/eligibility.py; drive listing, application submission, and reminders all consume that shared policy.
  • A new background task must be imported by Celery, run inside the Flask application context, define result retention expectations, and establish ownership before exposing results through shared task routes.
  • An internal file link must be fetched through the authenticated Axios client; ordinary anchor navigation does not attach the JWT bearer header.

Engineering roadmap

Resolve before deployment

  1. Predictable seed credentials are created automatically by every fresh database.
  2. SQLite assumes a single host. The Compose stack shares one database file across the web, worker, and Beat containers, which is adequate for a demo but should become PostgreSQL before real traffic.
  3. Uploaded and generated files need durable object storage, malware scanning, retention rules, and cleanup before handling untrusted production traffic.
  4. Reminder emails link to a hard-coded local development address.

Maintainability and scale

  1. Frontend component tests and database migrations are still missing.
  2. Most backend behavior lives in one application-factory function exceeding 1,500 lines.
  3. List endpoints have no pagination; dashboard searches are debounced on the client but still return unbounded result sets.
  4. The one-placement rule is enforced in request logic rather than a database constraint, leaving a race condition possible.

Deployment

Dockerfile builds in three stages: Node compiles the single-page application, uv resolves the locked Python environment, and the runtime stage copies both into a slim Python image that runs Gunicorn as the unprivileged portal user. Flask serves the compiled bundle from frontend/dist, so one image and one port cover the whole application.

docker-compose.yml runs that image three ways — Gunicorn, a Celery worker, and Beat — alongside Redis, with named volumes for the database, uploads, exports, and reports. Compose reads .env; APP_ENV is set to production, so a missing signing key stops the stack rather than falling back to a default.

Continuous integration

.github/workflows/ci.yml runs on every push and pull request to main:

Job Checks
backend-tests Ruff, then the full suite under coverage with coverage report enforcing the floor in pyproject.toml
authorization-tests Role boundaries, tenant isolation, protected file access, and secret handling as a standalone gate
secret-scan No fallback values for secret environment variables and no tracked .env file
frontend ESLint, the Vitest suite under its own coverage gate, the Vite production build, and npm audit on runtime dependencies
shell Bash syntax validation and ShellCheck at warning severity
docker Builds the image, boots the container, and polls /api/health

The coverage gate measures the request-handling path — app.py, models.py, eligibility.py, and email_helper.py. Celery tasks and the demo seeder are excluded because they are exercised by scripts/api-smoke.sh against a running stack rather than by unit tests.

Verification baseline

The maintained baseline is:

  • run.sh and scripts/api-smoke.sh pass Bash syntax validation and ShellCheck.
  • Ruff reports no findings and the backend tests pass on Python 3.13, holding the coverage floor.
  • scripts/api-smoke.sh passes against a locally running backend.
  • npm run lint and the Vite production build succeed.
  • npm audit reports no known dependency vulnerabilities.
  • The container image builds and answers /api/health.
  • Locked dependency environments remain excluded from version control.