GitHunt
ZA

zaxwebs/php-single-file-job-portal-tpl

TPL version of PSF Job Portal

Job Portal (PHP + SQLite)

A tiny, single-file job board + applicant tracker. Public visitors can browse open roles and apply; admins can post/edit jobs, toggle status, review applications, star candidates, and download resumes.

Stack: PHP 8+, SQLite, Bootstrap 5 (CDN).
Data: db.sqlite (auto-created), uploads saved to uploads/.


Features

  • Public listing of Open roles with search (title/type/location)
  • Job details page with Apply form (resume upload optional)
  • Admin login (password only), post/edit/close/restore/trash jobs
  • Soft-delete for jobs (Trash view), hard-delete for applications
  • Review applications per job or in a global table
  • Star / unstar candidates
  • Resume downloads (PDF/DOC/DOCX), 5 MB max
  • CSRF protection, HTML escaping, prepared statements, basic upload validation
  • Minimal, single-file deployment

Requirements

  • PHP 8.0+ (uses match, random_bytes, PDO SQLite, fileinfo)
    • Extensions: pdo_sqlite, fileinfo
  • Web server (or PHP built-in server)
  • Write permissions for the app directory (to create db.sqlite and uploads/)

Quick start

  1. Place the file

    • Save the provided PHP as index.php in an empty directory.
  2. Set an admin password (recommended)

    # Linux/macOS
    export JOBPORTAL_ADMIN='change-me-super-secret'
    # Windows PowerShell
    setx JOBPORTAL_ADMIN "change-me-super-secret"

    If not set, it falls back to DEFAULT_ADMIN_PASSWORD = 'password' in the file (change it!).

  3. Run locally

    php -S localhost:8000

    Open http://localhost:8000

  4. Log in as admin

    • Nav → Admin → enter your password.
    • Post your first job via Post a Job.

The app auto-creates db.sqlite (SQLite) and the uploads/ folder as needed.


Configuration

All configuration lives at the top of the file:

  • JOBPORTAL_ADMIN (env var): admin password (recommended way)
  • DEFAULT_ADMIN_PASSWORD: fallback if env var is not set
  • UPLOAD_DIR: defaults to ./uploads
  • MAX_UPLOAD_BYTES: default 5 MB
  • ALLOWED_EXTS: pdf, doc, docx
  • ALLOWED_MIME: matching MIME types

How it works (high-level)

  • Routing via ?action=... query param:
    • Public:
      list (default), view_job&id=…, apply_submit (POST)
    • Admin:
      login (GET/POST), logout, new_job, edit_job&id=…,
      create_job (POST), update_job (POST),
      toggle_job (POST), delete_job (POST), restore_job (POST),
      applications, view_application&id=…,
      delete_application (POST), toggle_star (POST),
      download&id=… (resume download)
  • Database (SQLite, WAL mode, foreign keys on):
    • jobs:
      • id, title, location, type, description, is_active, created_at, deleted_at
    • applications:
      • id, job_id (FK), name, email, phone, portfolio, resume_path, cover_letter, created_at, starred
    • Index: applications(job_id)
    • Simple migrations attempt to add new columns if missing.
  • Security:
    • CSRF tokens for all POSTs
    • htmlspecialchars() wrapper for all outputs
    • PDO prepared statements
    • Upload validation: ext + MIME + size, randomized filename, saved in uploads/

Using the app

Public

  • Home (/): Open roles (searchable), card list
  • Job detail: About the role + Apply form
    • Fields: name*, email*, phone, portfolio URL, optional resume file, cover letter
    • After submit, application is saved and an admin flash is shown

Admin

  • Login/Logout: password only
  • Post/Edit Job: title*, description*, type, location, active flag
  • Close/Reopen a job, Trash (soft delete) and Restore from Trash
  • Applications: per job or global table
    • Star/Unstar, Download resume, Delete application

Production notes

  • Set JOBPORTAL_ADMIN and consider removing/ignoring the DEFAULT_ADMIN_PASSWORD fallback.
  • HTTPS: serve behind HTTPS so admin sessions & uploads aren’t exposed.
  • Permissions:
    • The process must write to the app directory (or wherever db.sqlite / uploads/ live).
  • Uploads security:
    • Files are saved with random names and served through download action with a strict Content-Type.
    • To be extra cautious, block direct web access to the uploads/ directory or place it outside the web root and update UPLOAD_DIR.
    • Example Apache .htaccess to deny direct access:
      # uploads/.htaccess
      Require all denied
  • Backups: back up db.sqlite and uploads/.
  • Headers (optional hardening via server config):
    • X-Content-Type-Options: nosniff
    • Content-Security-Policy (if you tighten CDNs), etc.
  • Error display: disable in production:
    display_errors = Off
    log_errors = On

Known limitations / ideas

  • No email notifications to admins
  • No pagination (jobs/applications)
  • No rate limiting / CAPTCHA on Apply
  • Single admin password (no users/roles)
  • Files stored on local disk (no S3, etc.)

Routes reference

Action Method Purpose
list (default) GET Open roles (public). Admins can switch scopes: open, all, trash.
view_job&id={jobId} GET Job details + Apply form (if active).
apply_submit POST Submit application for a job.
login GET/POST Admin login.
logout GET End admin session.
new_job GET New job form (admin).
create_job POST Create job (admin).
edit_job&id={jobId} GET Edit job form (admin).
update_job POST Update job (admin).
toggle_job POST Open/close job (admin).
delete_job POST Soft-delete job (admin).
restore_job POST Restore a soft-deleted job (admin).
applications GET Global applications table (admin). Optional job_id filter.
view_application&id={appId} GET Application detail (admin).
toggle_star POST Star/Unstar application (admin).
delete_application POST Hard-delete application (admin).
download&id={appId} GET Download candidate resume (admin).

Customization

  • Update branding in page_header() (navbar title “Company Jobs”).
  • Adjust allowed file types and size at the top constants.
  • Change the list of job Types in job_form() if needed.

License

Choose your license (e.g., MIT) and add it here.