SYSTEM REFERENCE GUIDE · A 1 APP BUILDERS

A 1 App Builders - Ticket System

Complete technical reference for the self-hosted PHP support ticket system. File map, database schema, auth flows, email events, shared functions, and conventions — all in one document.

PHP 8.0+
SQLite3 · WAL mode
PHPMailer · SMTP
3 Roles · Admin / Agent / User
23 Files · Zero dependencies
01

Overview

The A 1 App Builders - Ticket System is a self-hosted, single-file-per-page PHP support ticketing application. It uses SQLite3 (no database server required), PHPMailer for transactional email, and a shared config.php that defines all constants, helpers, and the Database class.

There are three user roles — admin, agent, and user — each with their own auth guard and dashboard. Users register via email verification. Admins and agents are created by an admin.

🎫 Users

Self-register, verify email, submit tickets with category/priority/attachment, reply to their own tickets, track status changes.

🎧 Agents

View and reply to assigned tickets, add internal notes (hidden from users), change ticket status. Created by admin.

🛡 Admins

Full access — all tickets, user management, assign agents, change priorities/categories, settings, email toggles, color pickers.

📬 Emails

Four toggleable email events: new ticket, new reply, status change, assignment. Master on/off toggle plus per-event switches.

🗄 Database

SQLite3 only. Path: admin/database/ticket.db. WAL journal mode, foreign keys ON. Never use PDO in this project.

🔒 Security

CSRF on every POST form, bcrypt password hashing, email verification tokens, session timeout, secure cookie params, security headers.

Server path: /home/a1appbuilders/public_html/addons/ticketsystem/
Web root: /addons/ticketsystem (auto-detected in config.php)
02

File Structure

ticketsystem/ ├── index.php ← Landing page / logged-in redirect. Matches bannerconnect.php style. ├── login.php ← 3-tab login: User / Agent / Admin ├── forgot_password.php ← Step 1: request reset email. Step 2: set new password (token URL) ├── verify_email.php ← Email verification handler (token in query string) ├── ticketsystem.html ← This reference guide ├── README.md ← Plain-text install guide │ ├── admin/ │ ├── admin_dashboard.php ← Stats, recent tickets, pending users. Admin + agent. │ ├── admin_tickets.php ← All tickets. Filter by status/priority/category/search. Quick status + assign modals. │ ├── admin_ticket_view.php ← Full thread. Reply. Internal notes. Update panel (admin only). │ ├── admin_users.php ← User list. Activate, suspend, role change, create user modal. │ ├── admin_settings.php ← Email toggles, color pickers, categories, priorities. │ ├── access_denied.php ← 403 page shown by require_ts_admin() on role mismatch │ ├── uploads/ ← Agent reply attachments │ │ └── .htaccess ← No script execution │ ├── database/ │ │ ├── setup.php ← 4-tool setup panel (key-protected) │ │ ├── ticket.db ← Created by setup.php automatically │ │ └── .htaccess ← Deny from all (blocks DB download) │ └── includes/ │ └── config.php ← ALL constants, Database class, auth guards, helpers, mailer │ ├── assets/ │ ├── css/ │ │ └── login.css ← Login / register / forgot / verify styles. Indigo+cyan palette. │ └── uploads/ ← User ticket attachments │ └── .htaccess ← No script execution │ ├── includes/ │ └── logout.php ← Destroys session, logs action, redirects to login.php │ ├── logs/ │ └── .htaccess ← Deny from all │ └── users/ ├── user_dashboard.php ← User ticket list with stats + status filter pills ├── user_new_ticket.php ← Submit ticket: subject, category, priority, message, attachment ├── user_ticket_view.php ← Thread + reply form (no edit/delete). Internal notes hidden. └── user_register.php ← Self-registration + email verification send
!
Color key: Blue = public/auth files  ·  Yellow = admin pages  ·  White = support files
03

Installation

Requirements

RequirementMinimumNotes
PHP8.0+Uses named types, match, SQLite3 class
SQLite3 extensionAnyEnabled by default on most shared hosts
PHPMailer6.xInstalled via composer require phpmailer/phpmailer at site root
Filesystem writeadmin/database/, admin/uploads/, assets/uploads/, logs/ must be writable

Step-by-Step

1 · Upload

Upload the entire ticketsystem/ folder to /home/a1appbuilders/public_html/addons/

2 · PHPMailer

At site root, run composer require phpmailer/phpmailer. Or confirm vendor/autoload.php already exists.

3 · Run Setup

Visit /addons/ticketsystem/admin/database/setup.php?key=ticketsystem_setup_2025. Click Run Setup then Create Admin.

4 · Protect Setup

Delete or .htaccess-protect setup.php immediately after use.

5 · SMTP

Edit admin/includes/config.php: set SMTP_USER, SMTP_PASS (App Password), SMTP_HOST, SMTP_PORT.

6 · Login

Go to /addons/ticketsystem/login.php. Use the Admin tab with the credentials from Step 3.

Never leave setup.php accessible in production. It has a key guard, but delete it anyway — it can create/activate admin accounts.

Directory Permissions

chmod 755 admin/database/
chmod 755 admin/uploads/
chmod 755 assets/uploads/
chmod 755 logs/
04

Config & Constants

File: admin/includes/config.php — included by every PHP file in the system via require_once __DIR__ . '/../admin/includes/config.php' (or the equivalent relative path).

Identity

PROGRAM_NAME'ticketsystem'
SITE_NAME'A 1 App Builders'
ADMIN_EMAIL'support@a1appbuilders.com'

Path Constants (server filesystem)

ConstantResolves to
ADMIN_DIR…/ticketsystem/admin/
PROGRAM_DIR…/ticketsystem/
SITE_ROOT…/public_html/
DATABASE_DIRadmin/database/
DB_PATHadmin/database/ticket.db
ADMIN_UPLOADS_DIRadmin/uploads/
UPLOADS_DIRassets/uploads/
LOGS_DIRticketsystem/logs/

Web Path Constants

ConstantValue
WEB_ROOT/addons/ticketsystem (auto-detected)
ADMIN_WEB_ROOT/addons/ticketsystem/admin
USERS_WEB_ROOT/addons/ticketsystem/users
UPLOADS_WEB/addons/ticketsystem/assets/uploads
PLUGINS_WEB/plugins (Bootstrap, etc.)

Other Constants

MAX_FILE_SIZE5,242,880 bytes (5 MB)
SESSION_TIMEOUT1800 seconds (30 minutes)
DB_DEBUGfalse
SMTP_HOSTsmtp.gmail.com
SMTP_PORT587 (STARTTLS)
SMTP_USER / SMTP_PASSGmail address + App Password
05

Database Schema

Never use PDO anywhere in this project. Use $db = new Database(); $db->conn->prepare(…) only. The constructor/destructor manage the connection lifecycle automatically.
$db = new Database();
$stmt = $db->conn->prepare("SELECT * FROM tickets WHERE id=:id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);

Tables

TablePurposeKey Columns
usersAll accounts: admin, agent, userid, name, email, password_hash, role, status, email_verified, verify_token, reset_token
ticketsSupport ticket headersid, subject, category_id, priority_id, status, user_id, assigned_to, created_at, updated_at
ticket_repliesMessages in each ticket threadid, ticket_id, user_id, message, is_internal, attachment, created_at
categoriesTicket categoriesid, name, active
prioritiesPriority levels with colorsid, name, color (hex), active
settingsKey/value config storeid, setting_key (UNIQUE), setting_value, updated_at
activity_logUser and agent actionsid, username, action, created_at
login_logLogin attempts (success + fail)id, user_id, status, ip_address, user_agent, created_at
admin_logsAdmin-only actionsid, admin_name, action, ip_address, created_at

Ticket Status Values

open
in_progress
on_hold
resolved
closed

Default Categories

Seeded by setup.php: General · Billing · Technical · Account · Feature Request · Other

Default Priorities

NameColorHex
LowGreen#22c55e
MediumAmber#f59e0b
HighOrange#f97316
CriticalRed#ef4444
06

Auth & Session

Session Keys (after login)

$_SESSION['ts_user_id']int — users.id
$_SESSION['ts_name']string — users.name
$_SESSION['ts_email']string — users.email
$_SESSION['ts_role']'admin' | 'agent' | 'user'
$_SESSION['ts_last_activity']Unix timestamp — refreshed each request
$_SESSION['ts_csrf']string — 64-char hex CSRF token

Session Timeout — Two-Layer Enforcement

1 · Cookie Lifetime

session_set_cookie_params(['lifetime' => SESSION_TIMEOUT]) expires the browser cookie after 30 minutes of inactivity.

2 · Server-Side Check

After session_start(), compares ts_last_activity timestamp. If expired: unsets session, destroys, starts fresh session.

if (isset($_SESSION['ts_last_activity'])
    && (time() - $_SESSION['ts_last_activity']) > SESSION_TIMEOUT) {
    session_unset();
    session_destroy();
    if (session_status() === PHP_SESSION_NONE) session_start();
}
if (isset($_SESSION['ts_user_id'])) {
    $_SESSION['ts_last_activity'] = time();
}

Email Verification Flow

user_register.php
INSERT users (email_verified=0, status='pending')
Email sent with verify_token
verify_email.php?token=…
email_verified=1, status='active'
login.php?verified=1

Password Reset Flow

forgot_password.php (POST email)
SET reset_token + reset_expires (+1hr)
Email sent with token URL
forgot_password.php?token=…
New password set, token cleared
The forgot password email step always returns a success message regardless of whether the email exists — this prevents user enumeration attacks.
07

Roles & Guards

🛡 admin

  • All tickets (all users)
  • Update status / priority / category
  • Assign tickets to agents
  • Manage all users (activate, suspend, role change)
  • Create users/agents/admins
  • Settings — email, colors, categories, priorities
  • Add replies + internal notes

🎧 agent

  • View tickets assigned to them
  • Reply to assigned tickets
  • Add internal notes (hidden from users)
  • Change ticket status
  • Cannot change priority / assign / manage users

👤 user

  • Submit new tickets
  • View own tickets only
  • Add replies (no edit or delete ever)
  • See thread excluding internal notes
  • Cannot reply to closed tickets

Auth Guard Functions

FunctionAllowsRedirects to
require_ts_user()Any logged-in userlogin.php
require_ts_agent()agent + adminlogin.php
require_ts_admin()admin onlyadmin/access_denied.php
// Top of every admin page:
require_once __DIR__ . '/includes/config.php';
require_ts_admin();   // or require_ts_agent() for agent+admin pages

// Top of every user page:
require_once __DIR__ . '/../../admin/includes/config.php';  // adjust depth
require_ts_user();
08

Ticket Lifecycle

Submit Flow

user_new_ticket.php POST
INSERT tickets (status='open')
INSERT ticket_replies (first message)
send_ticket_email('new_ticket')
Redirect to user_ticket_view.php?submitted=1

Status Transitions

Any admin or agent can change status from admin_ticket_view.php (update panel) or the quick-modal on admin_tickets.php. A status change triggers a 'status_change' email to the user if that toggle is on.

open
in_progress
on_hold
resolved
closed

When a user replies to a resolved ticket, the status automatically reverts to open.

User Restrictions

Users cannot edit or delete any message after submission. A notice is shown on the reply form. Replies to closed tickets are blocked — user sees a closed-ticket banner with a link to open a new ticket.
09

Replies & Internal Notes

Typeis_internalVisible to userWho can add
User reply0YesUser (ticket owner)
Agent/Admin reply0YesAgent or Admin
Internal note1NoAgent or Admin only

Internal notes are excluded from the user's thread query via AND r.is_internal=0. They appear in the admin view with a dashed amber border and an "Internal" badge. They do not trigger user notification emails.

-- User-facing query (user_ticket_view.php)
SELECT r.*, u.name, u.role
  FROM ticket_replies r
  JOIN users u ON u.id = r.user_id
 WHERE r.ticket_id = :tid AND r.is_internal = 0
 ORDER BY r.created_at ASC;
10

Email Notifications

All email goes through send_ticket_email($db, $event, $data) in config.php, which checks the master toggle and per-event toggle before calling _dispatch_email()send_mail() (PHPMailer SMTP).

Event Map

Event keySettings keyRecipient(s)Trigger
'new_ticket'email_new_ticketUser (confirm) + Admin (alert)Ticket submitted via user_new_ticket.php
'new_reply'email_new_replyUser (or agent if user replied)Non-internal reply added by staff; or user reply notifies assigned agent/admin
'status_change'email_status_changeUserStatus changed in admin_ticket_view or admin_tickets quick modal
'assigned'email_assignedAgentassigned_to changed to a new agent

Master Toggle

Setting key: email_notifications. If '0', no emails are sent regardless of per-event toggles. All toggles are managed in Admin → Settings.

Email Template

If SITE_ROOT/assets/postoffice/email-template.html exists, it is used with placeholder substitution:

PlaceholderReplaced with
{{subject}}Email subject line
{{message}}HTML message body
{{fromEmail}}ADMIN_EMAIL
{{year}}date('Y')

If the template file does not exist, the raw HTML body is sent directly.

11

Attachments

Allowed Types

jpg · jpeg · png · gif · webp · pdf

Enforced by allowed_attachment($filename) which checks pathinfo(PATHINFO_EXTENSION) (lowercased). MIME type is not checked — extension is sufficient given the .htaccess blocks script execution in upload directories.

Size Limit

MAX_FILE_SIZE = 5,242,880 bytes (5 MB). Checked against $_FILES['attachment']['size'] before moving the file.

Storage Paths

UploaderServer pathWeb path
User (new ticket / reply)assets/uploads/UPLOADS_WEB
Agent / Admin (reply)admin/uploads/ADMIN_WEB_ROOT/uploads/

Filename Sanitization

$ext       = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$safe_name = 'u' . $me_id . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;

Original filenames are never used. Stored filename is saved in ticket_replies.attachment.

12

Admin Panel

Pages

PagePathAccessPurpose
Dashboardadmin/admin_dashboard.phpadmin + agentStats (total/open/resolved/my-tickets), recent tickets table, pending users panel
All Ticketsadmin/admin_tickets.phpadmin + agentFilterable ticket list. Quick status + assign modals. Agents see only assigned.
Ticket Viewadmin/admin_ticket_view.php?id=Nadmin + agentFull thread, reply form, internal note checkbox, update panel (admin only)
Usersadmin/admin_users.phpadmin onlyFilter/search users, activate/suspend, change role, create user modal
Settingsadmin/admin_settings.phpadmin onlyEmail toggles, ambient color pickers, add/toggle/delete categories and priorities

Quick Actions on admin_tickets.php

Both the Status and Assign buttons open Bootstrap modals with pre-filled selects. POST to the same page, process, then redirect with header('Location: …' . $_SERVER['QUERY_STRING']) to preserve filters.

Update Panel on admin_ticket_view.php

Visible to admins only (agents can only reply). Updates status, priority, category, and assigned agent in a single POST. Triggers status-change email if status differs from current, and assignment email if agent changed.

13

User Panel

PagePathPurpose
Dashboardusers/user_dashboard.phpStat cards (total/open/in-progress/resolved) + ticket table with status filter pills
New Ticketusers/user_new_ticket.phpSubject, category, priority, message textarea, optional attachment
Ticket Viewusers/user_ticket_view.php?id=NInfo strip, thread (no internal notes), reply form (no edit/delete), closed notice
Registerusers/user_register.phpName + email + password + confirm. Sends verification email. Status='pending' until verified.

Ownership Guard

user_ticket_view.php queries with WHERE t.id=:id AND t.user_id=:uid — if the ticket doesn't belong to the logged-in user, they are redirected to user_dashboard.php. Users can never access other users' tickets.

14

Settings Reference

All settings are stored in the settings table as key/value pairs. Read via get_setting($db, $key, $default). Written via set_setting($db, $key, $value) (upsert).

KeyDefaultDescription
site_nameA 1 App Builders - Ticket SystemShown in page titles and emails
email_notifications1Master email toggle — 0 disables all ticket emails
email_new_ticket1Send confirm to user + alert to admin on new ticket
email_new_reply1Notify user when agent replies
email_status_change1Notify user when ticket status changes
email_assigned1Notify agent when a ticket is assigned to them
accent_color#6366f1Primary indigo color
accent_color2#06b6d4Cyan accent
accent_warn#f59e0bWarning / yellow
accent_success#22c55eSuccess / green
15

Setup Tool

File: admin/database/setup.php — protected by a key param: ?key=ticketsystem_setup_2025. Returns 403 without it.

ToolPOST fieldWhat it does
1 — Run Setuprun_setupCreates all tables (IF NOT EXISTS), seeds default categories/priorities/settings. Safe to re-run.
2 — Create Admincreate_adminINSERT/UPSERT admin account. Sets role='admin', status='active', email_verified=1.
3 — Activate Useractivate_userSets status='active', email_verified=1 for any user by email.
4 — Delete Userdelete_userPermanently deletes non-admin user by email. Requires confirmation checkbox.

A recent-users table (last 20) is shown at the bottom of the page for quick verification.

Delete or restrict setup.php after installation. It is the only file in the system that can create admin accounts without an existing session.
16

Shared Functions

All functions live in admin/includes/config.php and are available everywhere config.php is included.

FunctionPurpose
ts_is_logged_in(): boolReturns true if ts_user_id and ts_role are set in session
require_ts_user()Redirect to login.php if not logged in
require_ts_agent()Redirect to login.php if not agent or admin
require_ts_admin()Redirect to access_denied.php if not admin
csrf_token(): stringReturn (or generate) the session CSRF token
verify_csrf(string $token): boolConstant-time compare of submitted vs session token
sanitize($v): stringhtmlspecialchars wrapper — use in all output
post(string $k): stringtrim($_POST[$k] ?? '')
get(string $k): stringtrim($_GET[$k] ?? '')
get_setting($db, $key, $default): stringRead value from settings table
set_setting($db, $key, $value): voidUpsert key/value into settings table
log_activity($db, $user, $action): voidInsert into activity_log
log_login($db, $uid, $status, $ip, $ua): voidInsert into login_log (uid may be null)
log_admin($db, $admin_name, $action, $ip): voidInsert into admin_logs
send_mail($to, $subject, $body): boolRaw PHPMailer SMTP send
send_ticket_email($db, $event, $data): voidCheck toggles, build email, dispatch for ticket events
_dispatch_email($tpl, $to, $subject, $msg): voidInternal — apply template and call send_mail
ticket_status_badge(string $status): stringReturn HTML badge for a ticket status
priority_badge(string $name, string $color): stringReturn colored pill badge for a priority
allowed_attachment(string $filename): boolCheck extension against allowed list (jpg/png/gif/webp/pdf)
17

Security

CSRF Protection

Every POST form includes <input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">. Every POST handler starts with if (!verify_csrf(post('csrf_token'))) { ... }. Token is stored in $_SESSION['ts_csrf'] and uses hash_equals() for constant-time comparison.

Security Headers

X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin

Password Storage

password_hash($pw, PASSWORD_DEFAULT) on register and admin create. Verified with password_verify() on login. Never stored in plain text.

.htaccess Protection

DirectoryProtection
admin/database/Deny from all — ticket.db cannot be downloaded
assets/uploads/Block PHP/script execution — files served statically only
admin/uploads/Block PHP/script execution — files served statically only
logs/Deny from all — log files not publicly accessible

SQL Injection Prevention

All queries use $db->conn->prepare() with bindValue() — never string interpolation of user input into queries. The only exception is integer IDs used directly in a small number of admin queries, which are always cast with (int) first.

Session Cookie Security

session_set_cookie_params([
    'lifetime' => SESSION_TIMEOUT,
    'path'     => '/',
    'secure'   => isset($_SERVER['HTTPS']),  // HTTPS-only when on HTTPS
    'httponly' => true,                        // Not accessible to JS
    'samesite' => 'Lax',
]);

session_regenerate_id(true) is called on every successful login to prevent session fixation.

18

Logs & Error Handling

PHP Error Log

ticketsystem/logs/php_errors.log — set in config.php via ini_set('error_log', …). Display errors are OFF; log errors are ON.

Activity Log

activity_log table via log_activity($db, $username, $action). Captures logins, ticket submissions, replies, and admin actions.

Login Log

login_log table via log_login(). Records successes and failures with IP, user agent, and status strings like 'fail_password', 'fail_unverified'.

Admin Log

admin_logs table via log_admin(). Every update to a ticket, user role change, or settings save is recorded with admin name and IP.

All DB operations and email sends are wrapped in try/catch. A logging failure or email error will never cause a 500 error or break the user flow — errors are written to the PHP error log only.
19

Rules & Conventions

Never use PDO. Use $db->conn->prepare() / bindValue() / fetchArray(SQLITE3_ASSOC) only.
Never interpolate user input into SQL. Always use prepared statements and bindValue.
Never echo unsanitized output. All user-supplied content must pass through sanitize() before being echoed to the page.
!
Always verify CSRF at the top of every POST handler before doing any database work.
Every file includes config.php first — it handles session start, security headers, DB class definition, and all helpers. Do not start sessions anywhere else.
Wrap all DB and mail calls in try/catch. Log errors with error_log(). Never let an exception surface to the user as a stack trace.

Coding Style

RuleExample
Input from POSTpost('key') — trims whitespace
Input from GETget('key') — trims whitespace
Output to HTMLsanitize($val) or = sanitize($row['col'])
Integer IDsCast with (int) before use in queries
Redirect after POSTheader('Location: …'); exit; — always include exit
Auth guard placementImmediately after require_once config.php — first executable line
TimezoneAmerica/Los_Angeles — set in config.php