30 Commits

Author SHA1 Message Date
wrongecho
58bcb38617 Add task approval system (tidy) 2026-01-12 12:21:10 +00:00
wrongecho
77e4d2b566 Add task approval system 2026-01-09 17:14:44 +00:00
wrongecho
88a29b7599 Bugfix: Mail queue loop not sending invoices to all billing contacts 2026-01-09 16:56:11 +00:00
wrongecho
64525750b6 Fix readme demo creds 2026-01-09 13:56:29 +00:00
wrongecho
30499123f1 Bugfix: Portal not showing contact user id in session 2026-01-09 13:50:46 +00:00
johnnyq
79703042ff Update client table responsiveness viewpoint to fix issue when window is a certain width on the desktop 2025-12-30 16:58:44 -05:00
johnnyq
ccd5605d97 Remove unused code 2025-12-29 18:03:53 -05:00
johnnyq
908277065b Fix Ticket Template auto filling for v1 ticket creation 2025-12-29 17:47:24 -05:00
johnnyq
f2d4eb0486 Fix Ticket Template auto filling 2025-12-29 17:46:23 -05:00
johnnyq
f784b659e8 Remove extra agent client wording 2025-12-20 15:05:47 -05:00
johnnyq
e60a7a59f9 Fix Login flow where user agent and client exists and agent has MFA but will not let them continue, also update some wording and button colors. Also dont show email password fields again after success and login as agent and client is shown. 2025-12-20 14:30:57 -05:00
johnnyq
cab81ca170 Fix Billable sort 2025-12-18 20:39:47 -05:00
johnnyq
a82e2c7ea1 Billable and non billable status use icons check and minus 2025-12-18 20:38:15 -05:00
johnnyq
a277380441 Set API key back to 32 Chars 2025-12-18 20:03:33 -05:00
johnnyq
ad5710b1d8 Fix Invoice CSV Exporting 2025-12-18 20:00:56 -05:00
johnnyq
3e3531a6ce Set API Key to 32 Chars 2025-12-18 14:28:24 -05:00
johnnyq
a79ce23ae5 Fix randomString() to generate cryptographically secure URL-safe tokens, reduced url keys to 32 Characters for performance and easy copy and paste and compatibility while still mainitaining ubreakable cryptographic keys 2025-12-18 14:24:53 -05:00
johnnyq
32f996d034 If login key is set and it is not provided show Client Email instead of just Email for placeholder 2025-12-14 13:42:38 -05:00
johnnyq
312eb4dffc Allow use of login key only for agents 2025-12-14 13:16:54 -05:00
johnnyq
1916456c84 Fix White Label not displaying on the login page 2025-12-14 13:04:53 -05:00
johnnyq
9b8d37b577 Updated changelog 2025-12-13 15:47:28 -05:00
johnnyq
05018e5f17 Added Reset favicon 2025-12-13 15:45:02 -05:00
johnnyq
72ef918452 Update changelog and bump app version to 25.12.1 2025-12-12 16:51:34 -05:00
johnnyq
27fde82aff Fixed Adding Payment provider not adding an account, now adding you can customize the income/expense account, expense category, and Expense vendor. Moved Saved Payment Provider Methods into Payment Providers as a link instead of on the admin side nav. Same with AI Provider and AI Models. 2025-12-12 16:42:09 -05:00
johnnyq
b27ffe6635 Refine DB Helpers 2025-12-10 18:32:46 -05:00
johnnyq
84cc4a094a Add DB helpers to make MySQLi Prepared statements less bloated and require less code 2025-12-10 17:09:34 -05:00
johnnyq
e75600ee05 Fix New ticket button in contact details in the related tickets section 2025-12-10 13:18:41 -05:00
johnnyq
871ad2ea7e Update legacy client login links to use the new unified login method 2025-12-10 12:21:12 -05:00
wrongecho
8b5f2e0f3f Update + credit 2025-12-10 08:54:24 +00:00
johnnyq
58d6ab7342 Unify Agent and Client login, if same user exists as a client and an agent then offer a selection of client portal or agent portal 2025-12-09 13:39:16 -05:00
55 changed files with 2163 additions and 983 deletions

View File

@@ -2,6 +2,21 @@
This file documents all notable changes made to ITFlow.
## [25.12.1] Maint Release
### Major Changes
- Unified the Client/Agent Login and process (Note only Client Users can Reset passwords from the login page, does not apply to agent users).
### Bug Fixes
- Fix Payment Provider not adding an account.
- Fix New ticket button in contact details in the related tickets section.
### New Features & Updates
- You can now Set Payment Provider income/expense account, expense vendor and expense category upond creation or editing.
- Moved Saved Payment Provider Methods away from admin side nav to the count link within Payment Providers page.
- Moved AI Models from the admin side nav to the model count link within AI Providers.
- Add Favicon Reset.
## [25.12] Stable Release
### Breaking Changes ###
@@ -319,7 +334,7 @@ We will provide example code with directory structure for each custom directory
---
### Fixed
- Several security vulnerabilities patched.
- Several security vulnerabilities patched (with thanks to www.helx.io).
- Ticket status is no longer updated when scheduling.
- Client Portal: Tech contacts can no longer edit their own details.
- Fixed overlapping logo issue in Invoice/Quote PDF exports.

View File

@@ -16,7 +16,7 @@
<br />
<a href="https://demo.itflow.org"><strong>View demo</strong></a>
<br />
Username: <b>demo@demo</b> | Password: <b>demo</b>
Username: <b>demo@demo.com</b> | Password: <b>demo</b>
<br />
<br />
<a href="https://itflow.org/#about">About</a>

View File

@@ -13,7 +13,7 @@ We operate a rolling release model. Any bug fixes will be released into latest v
| Version | Supported |
|---------| ------------------ |
| 25.05 | :white_check_mark: |
| 25.12 | :white_check_mark: |
## Reporting a Vulnerability via GitHub Security Advisories

View File

@@ -12,6 +12,16 @@ $num_rows = mysqli_num_rows($sql);
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="/admin">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="ai_provider.php">AI Providers</a>
</li>
<li class="breadcrumb-item active">AI Models</li>
</ol>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-robot mr-2"></i>AI Models</h3>

View File

@@ -39,7 +39,7 @@ $num_rows = mysqli_num_rows($sql);
Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?>
</a>
</th>
<th>
<th class="text-center">
<a class="text-dark">Models</a>
</th>
<th class="text-center">Action</th>
@@ -67,7 +67,8 @@ $num_rows = mysqli_num_rows($sql);
</td>
<td><?php echo $url; ?></td>
<td><?php echo $key; ?></td>
<td><?php echo $ai_model_count; ?></td>
<td class="text-center">
<a class="badge badge-dark badge-pill p-2" href="ai_model.php"><?= $ai_model_count ?></a>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">

View File

@@ -4134,10 +4134,30 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.8'");
}
// if (CURRENT_DATABASE_VERSION == '2.3.8') {
// // Insert queries here required to update to DB version 2.3.9
if (CURRENT_DATABASE_VERSION == '2.3.8') {
mysqli_query($mysqli, "
CREATE TABLE `task_approvals` (
`approval_id` int(11) NOT NULL AUTO_INCREMENT,
`approval_scope` enum('client','internal') NOT NULL,
`approval_type` enum('any','technical','billing','specific') NOT NULL,
`approval_required_user_id` int(11) DEFAULT NULL,
`approval_status` enum('pending','approved','declined') NOT NULL,
`approval_created_by` int(11) NOT NULL,
`approval_approved_by` varchar(255) DEFAULT NULL,
`approval_url_key` varchar(200) NOT NULL,
`approval_task_id` int(11) NOT NULL,
PRIMARY KEY (`approval_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.9'");
}
// if (CURRENT_DATABASE_VERSION == '2.3.9') {
// // Insert queries here required to update to DB version 2.4.0
// // Then, update the database to the next sequential version
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.9'");
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.0'");
// }
} else {

View File

@@ -69,30 +69,20 @@
</a>
</li>
<li class="nav-item">
<a href="/admin/payment_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'payment_provider.php' ? 'active' : ''); ?>">
<a href="/admin/payment_provider.php"
class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['payment_provider.php', 'saved_payment_method.php']) ? 'active' : ''); ?>">
<i class="nav-icon far fa-credit-card"></i>
<p>Payment Providers</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/saved_payment_method.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'saved_payment_method.php' ? 'active' : ''); ?>">
<i class="nav-icon far fa-credit-card"></i>
<p>Saved Payments</p>
</a>
</li>
<?php } ?>
<li class="nav-item">
<a href="/admin/ai_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ai_provider.php' ? 'active' : ''); ?>">
<a href="/admin/ai_provider.php"
class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['ai_provider.php', 'ai_model.php']) ? 'active' : ''); ?>">
<i class="nav-icon fas fa-robot"></i>
<p>AI Providers</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/ai_model.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ai_model.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-robot"></i>
<p>AI Models</p>
</a>
</li>
<?php if ($config_module_enable_ticketing) { ?>
<li class="nav-item">

View File

@@ -2,8 +2,8 @@
require_once '../../../includes/modal_header.php';
$key = randomString(156);
$decryptPW = randomString(160);
$key = randomString(32);
$decryptPW = randomString(32);
ob_start();
?>

View File

@@ -16,85 +16,182 @@ ob_start();
<div class="modal-body">
<div class="alert alert-info">
An income account named after the provider will always be created and used for income of paid invoices.<br>
If "Enable Expense" option is enabled, a matching vendor will also be automatically created for expense tracking. Additionally, an expense category named "Payment Processing" will be created.
<div class="alert alert-info text-center">
<h6>Before Adding a Payment Provider!</h6>
We recommend you add an <strong>Account</strong> and <strong>Vendor</strong> based off the Provider name before continuing eg <strong>Stripe</strong>
</div>
<div class="form-group">
<label>Provider <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-credit-card"></i></span>
</div>
<select class="form-control select2" name="provider">
<option>Stripe</option>
</select>
</div>
</div>
<div class="form-group">
<label>Publishable key <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)">
</div>
</div>
<div class="form-group">
<label>Secret key <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)">
</div>
</div>
<div class="form-group">
<label>Threshold</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00">
</div>
<small class="form-text text-muted">Will not show as an option at Checkout if invoice amount is above this number, 0 disables the threshold check.</small>
</div>
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-toggle="pill" href="#pills-details">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-expense">Expense</a>
</li>
</ul>
<hr>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" name="enable_expense" checked value="1" id="enableExpenseSwitch">
<label class="custom-control-label" for="enableExpenseSwitch">Enable Expense</label>
</div>
</div>
<div class="tab-content">
<div class="form-group">
<label>Percentage Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
<div class="tab-pane fade show active" id="pills-details">
<div class="form-group">
<label>Provider <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-credit-card"></i></span>
</div>
<select class="form-control select2" name="provider">
<option>Stripe</option>
</select>
</div>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" placeholder="Enter Percentage">
</div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div>
<div class="form-group">
<label>Flat Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
<div class="form-group">
<label>Publishable key <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)">
</div>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" placeholder="0.030">
</div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div>
<div class="form-group">
<label>Secret key <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)">
</div>
</div>
<div class="form-group">
<label>Income / Expense Account <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
</div>
<select class="form-control select2" name="account" required>
<option value="">- Select an Account -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT account_id, account_name FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($row = mysqli_fetch_array($sql)) {
$account_id = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']);
?>
<option <?php if ($account_name === 'Stripe') { echo "selected"; } ?> value="<?= $account_id ?>"><?= $account_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="form-group">
<label>Threshold</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00">
</div>
<small class="form-text text-muted">Will not show as an option at Checkout if invoice amount is above this number, 0 disables the threshold check.</small>
</div>
</div>
<div class="tab-pane fade" id="pills-expense">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" name="enable_expense" checked value="1" id="enableExpenseSwitch">
<label class="custom-control-label" for="enableExpenseSwitch">Enable Expense</label>
</div>
</div>
<div class="form-group">
<label>Payment Provider Vendor <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
</div>
<select class="form-control select2" name="expense_vendor" required>
<option value="0">Expense Disabled</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
while ($row = mysqli_fetch_array($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = nullable_htmlentities($row['vendor_name']);
?>
<option <?php if ($vendor_name === 'Stripe') { echo "selected"; } ?> value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="form-group">
<label>Expense Category <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
</div>
<select class="form-control select2" name="expense_category" required>
<option value="">- Select a Category -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Expense' AND category_archived_at IS NULL ORDER BY category_name ASC");
while ($row = mysqli_fetch_array($sql)) {
$category_id = intval($row['category_id']);
$category_name = nullable_htmlentities($row['category_name']);
?>
<option <?php if ($category_name === 'Processing Fee') { echo "selected"; } ?> value="<?= $category_id ?>"><?= $category_name ?></option>
<?php
}
?>
</select>
<div class="input-group-append">
<button class="btn btn-secondary ajax-modal" type="button"
data-modal-url="../admin/modals/category/category_add.php?category=Expense">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>Percentage Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" placeholder="Enter Percentage">
</div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div>
<div class="form-group">
<label>Flat Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" placeholder="0.030">
</div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_payment_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Add</button>

View File

@@ -10,10 +10,10 @@ $row = mysqli_fetch_array($sql);
$provider_name = nullable_htmlentities($row['payment_provider_name']);
$public_key = nullable_htmlentities($row['payment_provider_public_key']);
$private_key = nullable_htmlentities($row['payment_provider_private_key']);
$account_id = nullable_htmlentities($row['payment_provider_account']);
$account_id = intval($row['payment_provider_account']);
$threshold = floatval($row['payment_provider_threshold']);
$vendor_id = nullable_htmlentities($row['payment_provider_expense_vendor']);
$category_id = nullable_htmlentities($row['payment_provider_expense_category']);
$vendor_id = intval($row['payment_provider_expense_vendor']);
$category_id = intval($row['payment_provider_expense_category']);
$percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100;
$flat_fee = floatval($row['payment_provider_expense_flat_fee']);
@@ -21,80 +21,170 @@ $flat_fee = floatval($row['payment_provider_expense_flat_fee']);
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Editing: <strong><?php echo $provider_name; ?></strong></h5>
<h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Editing: <strong><?= $provider_name ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="provider_id" value="<?php echo $provider_id; ?>">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="provider_id" value="<?= $provider_id ?>">
<div class="modal-body">
<div class="form-group">
<label>Publishable key <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?php echo $public_key; ?>">
</div>
</div>
<div class="form-group">
<label>Secret key <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)" value="<?php echo $private_key; ?>">
</div>
</div>
<div class="form-group">
<label>Threshold</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00" value="<?php echo $threshold; ?>">
</div>
<small class="form-text text-muted">Will not show as an option at Checkout if above this number</small>
</div>
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-toggle="pill" href="#pills-details">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-expense">Expense</a>
</li>
</ul>
<hr>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" name="enable_expense" <?php if ($vendor_id) { echo "checked"; } ?> value="1" id="enableEditExpenseSwitch">
<label class="custom-control-label" for="enableEditExpenseSwitch">Enable Expense</label>
</div>
<small>(Category: Payment Processing -- Vendor: <?php echo $provider_name; ?></small>
</div>
<div class="tab-content">
<div class="form-group">
<label>Percentage Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
<div class="tab-pane fade show active" id="pills-details">
<div class="form-group">
<label>Publishable key <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?= $public_key ?>">
</div>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" value="<?php echo $percent_fee; ?>" placeholder="Enter Percentage">
</div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div>
<div class="form-group">
<label>Flat Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
<div class="form-group">
<label>Secret key <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div>
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)" value="<?= $private_key ?>">
</div>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" value="<?php echo $flat_fee; ?>" placeholder="0.030">
</div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div>
<div class="form-group">
<label>Income / Expense Account <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
</div>
<select class="form-control select2" name="account" required>
<option value="">- Select an Account -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT account_id, account_name FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($row = mysqli_fetch_array($sql)) {
$account_id_select = intval($row['account_id']);
$account_name = nullable_htmlentities($row['account_name']);
?>
<option <?php if ($account_id === $account_id_select) { echo "selected"; } ?> value="<?= $account_id_select ?>"><?= $account_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="form-group">
<label>Threshold</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00" value="<?php echo $threshold; ?>">
</div>
<small class="form-text text-muted">Will not show as an option at Checkout if above this number</small>
</div>
</div>
<div class="tab-pane fade" id="pills-expense">
<div class="form-group">
<label>Payment Provider Vendor <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
</div>
<select class="form-control select2" name="expense_vendor" required>
<option value="0">Expense Disabled</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
while ($row = mysqli_fetch_array($sql)) {
$vendor_id_select = intval($row['vendor_id']);
$vendor_name = nullable_htmlentities($row['vendor_name']);
?>
<option <?php if ($vendor_id === $vendor_id_select) { echo "selected"; } ?>
value="<?= $vendor_id_select ?>"><?= $vendor_name ?>
</option>
<?php
}
?>
</select>
</div>
</div>
<div class="form-group">
<label>Expense Category <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
</div>
<select class="form-control select2" name="expense_category" required>
<option value="">- Select a Category -</option>
<?php
$sql_category = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Expense' AND category_archived_at IS NULL ORDER BY category_name ASC");
while ($row = mysqli_fetch_array($sql_category)) {
$category_id_select = intval($row['category_id']);
$category_name = nullable_htmlentities($row['category_name']);
?>
<option <?php if ($category_id === $category_id_select) { echo "selected"; } ?> value="<?= $category_id_select ?>"><?= $category_name ?></option>
<?php
}
?>
</select>
<div class="input-group-append">
<button class="btn btn-secondary ajax-modal" type="button"
data-modal-url="../admin/modals/category/category_add.php?category=Expense">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>Percentage Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" value="<?php echo $percent_fee; ?>" placeholder="Enter Percentage">
</div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div>
<div class="form-group">
<label>Flat Fee to expense</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
</div>
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" value="<?php echo $flat_fee; ?>" placeholder="0.030">
</div>
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_payment_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>

View File

@@ -6,10 +6,10 @@ $order = "ASC";
require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers
$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers
LEFT JOIN accounts ON payment_provider_account = account_id
LEFT JOIN vendors ON payment_provider_expense_vendor = vendor_id
LEFT JOIN categories ON payment_provider_expense_category = category_id
LEFT JOIN categories ON payment_provider_expense_category = category_id
ORDER BY $sort $order"
);
@@ -57,7 +57,7 @@ $num_rows = mysqli_num_rows($sql);
<th>
<a class="text-dark">Expensed Fee</a>
</th>
<th>
<th class="text-center">
<a class="text-dark">Saved Payment Methods</a>
</th>
<th class="text-center">Action</th>
@@ -72,7 +72,7 @@ $num_rows = mysqli_num_rows($sql);
$provider_description = nullable_htmlentities($row['payment_provider_description']);
$account_name = nullable_htmlentities($row['account_name']);
$threshold = floatval($row['payment_provider_threshold']);
$vendor_name = nullable_htmlentities($row['vendor_name']);
$vendor_name = nullable_htmlentities($row['vendor_name'] ?? "Expense Disabled");
$category = nullable_htmlentities($row['category_name']);
$percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100;
$flat_fee = floatval($row['payment_provider_expense_flat_fee']);
@@ -94,7 +94,9 @@ $num_rows = mysqli_num_rows($sql);
<td><?php echo $vendor_name; ?></td>
<td><?php echo $category; ?></td>
<td><?php echo $percent_fee; ?>% + <?php echo numfmt_format_currency($currency_format, $flat_fee, $session_company_currency); ?></td>
<td><?php echo $saved_payment_count; ?></td>
<td class="text-center">
<a class="badge badge-dark badge-pill p-2" href="saved_payment_method.php"><?= $saved_payment_count ?></a>
</td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">

View File

@@ -49,7 +49,7 @@ if (isset($_POST['edit_ai_model'])) {
if (isset($_GET['delete_ai_model'])) {
validateCSRFToken($_GET['csrf_token']);
$model_id = intval($_GET['delete_ai_model']);
$model_name = sanitizeInput(getFieldById('ai_models', $model_id, 'ai_model_name'));

View File

@@ -14,53 +14,20 @@ if (isset($_POST['add_payment_provider'])) {
$public_key = sanitizeInput($_POST['public_key']);
$private_key = sanitizeInput($_POST['private_key']);
$threshold = floatval($_POST['threshold']);
$enable_expense = intval($_POST['enable_expense'] ?? 0);
$account = intval($_POST['account']);
$expense_vendor = intval($_POST['expense_vendor']) ?? 0;
$expense_category = intval($_POST['expense_category']) ?? 0;
$percentage_fee = floatval($_POST['percentage_fee']) / 100 ?? 0;
$flat_fee = floatval($_POST['flat_fee']) ?? 0;
// Check to ensure provider isn't added twice
$sql = "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1";
$result = mysqli_query($mysqli, $sql);
if (mysqli_num_rows($result) > 0) {
$sql = mysqli_query($mysqli, "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1");
if (mysqli_num_rows($sql) > 0) {
flash_alert("Payment Provider <strong>$provider</strong> already exists", 'error');
redirect();
}
// Check for Stripe Account, if not create it
$sql_account = mysqli_query($mysqli,"SELECT account_id FROM accounts WHERE account_name = '$provider' AND account_archived_at IS NULL LIMIT 1");
if (mysqli_num_rows($sql_account) == 0) {
$account_id = mysqli_insert_id($mysqli);
} else {
$row = mysqli_fetch_array($sql_account);
$account_id = intval($row['account_id']);
}
// Expense defaults
$category_id = 0;
$vendor_id = 0;
if ($enable_expense) {
// Category
$sql_category = mysqli_query($mysqli,"SELECT category_id FROM categories WHERE category_name = 'Payment Processing' AND category_type = 'Expense' AND category_archived_at IS NULL LIMIT 1");
if (mysqli_num_rows($sql_category) == 0) {
mysqli_query($mysqli,"INSERT INTO categories SET category_name = 'Processing Fee', category_type = 'Payment Processing', category_color = 'gray'");
$category_id = mysqli_insert_id($mysqli);
} else {
$row = mysqli_fetch_array($sql_category);
$category_id = intval($row['category_id']);
}
// Vendor
$sql_vendor = mysqli_query($mysqli,"SELECT vendor_id FROM vendors WHERE vendor_name = '$provider' AND vendor_client_id = 0 AND vendor_archived_at IS NULL LIMIT 1");
if (mysqli_num_rows($sql_vendor) == 0) {
mysqli_query($mysqli,"INSERT INTO vendors SET vendor_name = '$provider', vendor_description = 'Payment Processor Provider', vendor_client_id = 0");
$vendor_id = mysqli_insert_id($mysqli);
} else {
$row = mysqli_fetch_array($sql_vendor);
$vendor_id = intval($row['vendor_id']);
}
}
mysqli_query($mysqli,"INSERT INTO payment_providers SET payment_provider_name = '$provider', payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_account = $account_id, payment_provider_expense_vendor = $vendor_id, payment_provider_expense_category = $category_id, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee");
mysqli_query($mysqli,"INSERT INTO payment_providers SET payment_provider_name = '$provider', payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_account = $account, payment_provider_expense_vendor = $expense_vendor, payment_provider_expense_category = $expense_category, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee");
$provider_id = mysqli_insert_id($mysqli);
@@ -81,11 +48,13 @@ if (isset($_POST['edit_payment_provider'])) {
$public_key = sanitizeInput($_POST['public_key']);
$private_key = sanitizeInput($_POST['private_key']);
$threshold = floatval($_POST['threshold']);
$enable_expense = intval($_POST['enable_expense'] ?? 0);
$account = intval($_POST['account']);
$expense_vendor = intval($_POST['expense_vendor']) ?? 0;
$expense_category = intval($_POST['expense_category']) ?? 0;
$percentage_fee = floatval($_POST['percentage_fee']) / 100;
$flat_fee = floatval($_POST['flat_fee']);
mysqli_query($mysqli,"UPDATE payment_providers SET payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee WHERE payment_provider_id = $provider_id");
mysqli_query($mysqli,"UPDATE payment_providers SET payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_account = $account, payment_provider_expense_vendor = $expense_vendor, payment_provider_expense_category = $expense_category, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee WHERE payment_provider_id = $provider_id");
logAction("Payment Provider", "Edit", "$session_name edited Payment Provider $provider");
@@ -98,7 +67,7 @@ if (isset($_POST['edit_payment_provider'])) {
if (isset($_GET['delete_payment_provider'])) {
validateCSRFToken($_GET['csrf_token']);
$provider_id = intval($_GET['delete_payment_provider']);
// When deleted it cascades deletes

View File

@@ -49,3 +49,17 @@ if (isset($_POST['edit_favicon_settings'])) {
redirect();
}
if (isset($_GET['reset_favicon'])) {
if (file_exists("../uploads/favicon.ico")) {
unlink("../uploads/favicon.ico");
}
logAction("Settings", "Edit", "$session_name reset Favicon");
flash_alert("Favicon reset", 'error');
redirect();
}

View File

@@ -13,18 +13,18 @@ $sql = mysqli_query($mysqli, "
clients.client_name,
client_payment_provider.payment_provider_client
FROM client_saved_payment_methods
LEFT JOIN payment_providers
LEFT JOIN payment_providers
ON client_saved_payment_methods.saved_payment_provider_id = payment_providers.payment_provider_id
LEFT JOIN clients
LEFT JOIN clients
ON client_saved_payment_methods.saved_payment_client_id = clients.client_id
LEFT JOIN client_payment_provider
ON client_payment_provider.client_id = client_saved_payment_methods.saved_payment_client_id
AND client_payment_provider.payment_provider_id = client_saved_payment_methods.saved_payment_provider_id
WHERE
client_name LIKE '%$q%'
OR payment_provider_name LIKE '%$q%'
OR saved_payment_description LIKE '%$q%'
OR payment_provider_client LIKE '%$q%'
WHERE
client_name LIKE '%$q%'
OR payment_provider_name LIKE '%$q%'
OR saved_payment_description LIKE '%$q%'
OR payment_provider_client LIKE '%$q%'
OR saved_payment_provider_method LIKE '%$q%'
ORDER BY $sort $order
");
@@ -33,6 +33,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="/admin">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="payment_provider.php">Payment Providers</a>
</li>
<li class="breadcrumb-item active">Saved Payment Methods (Stripe)</li>
</ol>
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-fw fa-credit-card mr-2"></i>Saved Payment Methods</h3>
@@ -107,8 +117,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<tr>
<td><?php echo $client_name; ?> (<?php echo $client_id; ?>)</td>
<td><?php echo $provider_name; ?> (<?php echo $provider_id; ?>)</td>
<td>
<?= $client_name ?>
<br>
<small class="text-secondary">ID: <?= $client_id ?></small>
</td>
<td>
<?= $provider_name ?>
<br>
<small class="text-secondary">ID: <?= $provider_id ?></small>
</td>
<td><?php echo $saved_payment_description; ?></td>
<td><?php echo $provider_client; ?></td>
<td><?php echo $provider_payment_method; ?></td>

View File

@@ -57,11 +57,12 @@ require_once "includes/inc_all_admin.php";
<hr>
<button type="submit" name="edit_favicon_settings" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Upload Icon</button>
<?php if(file_exists("../uploads/favicon.ico")) { ?>
<a href="post.php?reset_favicon" class="btn btn-outline-danger"><i class="fas fa-redo-alt mr-2"></i>Reset Favicon</a>
<?php } ?>
</form>
</div>
</div>
<?php
require_once "../includes/footer.php";

View File

@@ -49,7 +49,7 @@ if (isset($_GET['merge_ticket_get_json_details'])) {
$merge_into_ticket_number = intval(preg_replace('/[^0-9]/', '', $_GET['merge_into_ticket_number']));
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_number, ticket_prefix, ticket_subject, ticket_priority, ticket_status, ticket_status_name, client_name, contact_name FROM tickets
LEFT JOIN clients ON ticket_client_id = client_id
LEFT JOIN clients ON ticket_client_id = client_id
LEFT JOIN contacts ON ticket_contact_id = contact_id
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
WHERE ticket_number = $merge_into_ticket_number");
@@ -86,7 +86,7 @@ if (isset($_POST['contact_set_notes'])) {
$notes = sanitizeInput($_POST['notes']);
// Get Contact Details and Client ID for Logging
$sql = mysqli_query($mysqli,"SELECT contact_name, contact_client_id
$sql = mysqli_query($mysqli,"SELECT contact_name, contact_client_id
FROM contacts WHERE contact_id = $contact_id"
);
$row = mysqli_fetch_array($sql);
@@ -108,7 +108,7 @@ if (isset($_POST['asset_set_notes'])) {
$notes = sanitizeInput($_POST['notes']);
// Get Asset Details and Client ID for Logging
$sql = mysqli_query($mysqli,"SELECT asset_name, asset_client_id
$sql = mysqli_query($mysqli,"SELECT asset_name, asset_client_id
FROM assets WHERE asset_id = $asset_id"
);
$row = mysqli_fetch_array($sql);
@@ -195,7 +195,7 @@ if (isset($_GET['share_generate_link'])) {
$item_expires_friendly = "1 month";
}
$item_key = randomString(156);
$item_key = randomString(32);
if ($item_type == "Document") {
$row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT document_name FROM documents WHERE document_id = $item_id AND document_client_id = $client_id LIMIT 1"));
@@ -496,8 +496,8 @@ if (isset($_POST['update_kanban_ticket'])) {
if (!empty($config_smtp_host) && $config_ticket_client_general_notifications == 1) {
// Get details
$ticket_sql = mysqli_query($mysqli, "SELECT contact_name, contact_email, ticket_prefix, ticket_number, ticket_subject, ticket_status_name, ticket_assigned_to, ticket_url_key, ticket_client_id FROM tickets
LEFT JOIN clients ON ticket_client_id = client_id
$ticket_sql = mysqli_query($mysqli, "SELECT contact_name, contact_email, ticket_prefix, ticket_number, ticket_subject, ticket_status_name, ticket_assigned_to, ticket_url_key, ticket_client_id FROM tickets
LEFT JOIN clients ON ticket_client_id = client_id
LEFT JOIN contacts ON ticket_contact_id = contact_id
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
WHERE ticket_id = $ticket_id
@@ -905,7 +905,7 @@ if (isset($_GET['ai_ticket_summary'])) {
}
$prompt = "
Summarize the following IT support ticket and its responses in a concise, clear, and professional manner.
Summarize the following IT support ticket and its responses in a concise, clear, and professional manner.
The summary should include:
1. Main Issue: What was the problem reported by the user?
@@ -992,3 +992,23 @@ if (isset($_GET['apex_domain_check'])) {
echo json_encode($response);
}
// Get internal users/techs
if (isset($_GET['get_internal_users'])) {
enforceUserPermission('module_support');
$sql = mysqli_query(
$mysqli,
"SELECT user_id, user_name
FROM users
WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL
ORDER BY user_name"
);
while ($row = mysqli_fetch_assoc($sql)) {
$response['users'][] = $row;
}
echo json_encode($response);
exit;
}

View File

@@ -1,72 +0,0 @@
<?php require_once "includes/inc_all.php"; ?>
<!-- Breadcrumbs-->
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="index.html">Dashboard</a>
</li>
<li class="breadcrumb-item active">Blank Page</li>
</ol>
<!-- Page Content -->
<h1>Blank Page</h1>
<hr>
<p>This is a great starting point for new custom pages.</p>
<h1><?php echo $session_user_role; ?></h1>
<?php validateAdminRole(); ?>
<?php
$start_date = date('Y') . "-10-10";
echo "<H1>$start_date</H1>";
echo "<H2>User Agent</H2>";
echo getUserAgent();
?>
<br>
<input type="tel" name="phone" id="phone">
<div class="form-group">
<label>Minimal</label>
<select class="form-control select2 select2-hidden-accessible" style="width: 100%;" data-select2-id="1" tabindex="-1" aria-hidden="true">
<option selected="selected" data-select2-id="3">Alabama</option>
<option data-select2-id="35">Alaska</option>
<option data-select2-id="36">California</option>
<option data-select2-id="37">Delaware</option>
<option data-select2-id="38">Tennessee</option>
<option data-select2-id="39">Texas</option>
<option data-select2-id="40">Washington</option>
</select><span class="select2 select2-container select2-container--default select2-container--below" dir="ltr" data-select2-id="2" style="width: 100%;"><span class="selection"><span class="select2-selection select2-selection--single" role="combobox" aria-haspopup="true" aria-expanded="false" tabindex="0" aria-disabled="false" aria-labelledby="select2-nbex-container"><span class="select2-selection__rendered" id="select2-nbex-container" role="textbox" aria-readonly="true" title="Alabama">Alabama</span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span></span></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>
</div>
<dl>
<dt>Requester</dt>
<dd>Sam Adams</dd>
<dt>Created</dt>
<dd><time datetime="2024-04-11T17:52:30+00:00" title="2024-04-11 13:52" data-datetime="calendar">Today at 13:52</time></dd>
<dt>Last activity</dt>
<dd><time datetime="2024-04-11T18:08:55+00:00" title="2024-04-11 14:08" data-datetime="calendar">Today at 14:08</time></dd>
</dl>
<?php echo randomString(100); ?>
<br>
<textarea class="tinymceTest"></textarea>
<textarea class="tinymce"></textarea>
<textarea class="tinymceTicket"></textarea>
<?php
// show the current Date and Time
$date_time = date('Y-m-d H:i:s');
echo "Current Date and Time: <strong>$date_time</strong>";
?>
<script>toastr.success('Have Fun Wozz!!')</script>
<?php require_once "../includes/footer.php";

View File

@@ -286,7 +286,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="table-responsive-sm">
<div class="table-responsive">
<table class="table table-hover mb-0 text-nowrap">
<thead class="<?php if ($num_rows[0] == 0) { echo "d-none"; } ?> bg-light">
<tr>

View File

@@ -14,7 +14,7 @@ if (isset($_GET['client_id'])) {
if (isset($_GET['contact_id'])) {
$contact_id = intval($_GET['contact_id']);
$sql = mysqli_query($mysqli, "SELECT * FROM contacts
$sql = mysqli_query($mysqli, "SELECT * FROM contacts
LEFT JOIN clients ON client_id = contact_client_id
LEFT JOIN locations ON location_id = contact_location_id
LEFT JOIN users ON user_id = contact_user_id
@@ -76,7 +76,7 @@ if (isset($_GET['contact_id'])) {
// Linked Software Licenses
$sql_linked_software = mysqli_query($mysqli, "SELECT * FROM software_contacts, software
WHERE software_contacts.contact_id = $contact_id
WHERE software_contacts.contact_id = $contact_id
AND software_contacts.software_id = software.software_id
AND software_archived_at IS NULL
ORDER BY software_name ASC"
@@ -109,7 +109,7 @@ if (isset($_GET['contact_id'])) {
$ticket_count = mysqli_num_rows($sql_related_tickets);
// Related Recurring Tickets Query
$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT * FROM recurring_tickets
$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT * FROM recurring_tickets
WHERE recurring_ticket_contact_id = $contact_id
ORDER BY recurring_ticket_next_run DESC"
);
@@ -144,7 +144,7 @@ if (isset($_GET['contact_id'])) {
// Linked Services
$sql_linked_services = mysqli_query($mysqli, "SELECT * FROM service_contacts, services
WHERE service_contacts.contact_id = $contact_id
WHERE service_contacts.contact_id = $contact_id
AND service_contacts.service_id = services.service_id
ORDER BY service_name ASC"
);
@@ -155,7 +155,7 @@ if (isset($_GET['contact_id'])) {
// Linked Documents
$sql_linked_documents = mysqli_query($mysqli, "SELECT * FROM contact_documents, documents
LEFT JOIN users ON document_created_by = user_id
WHERE contact_documents.contact_id = $contact_id
WHERE contact_documents.contact_id = $contact_id
AND contact_documents.document_id = documents.document_id
AND document_archived_at IS NULL
ORDER BY document_name ASC"
@@ -166,7 +166,7 @@ if (isset($_GET['contact_id'])) {
// Linked Files
$sql_linked_files = mysqli_query($mysqli, "SELECT * FROM contact_files, files
WHERE contact_files.contact_id = $contact_id
WHERE contact_files.contact_id = $contact_id
AND contact_files.file_id = files.file_id
AND file_archived_at IS NULL
ORDER BY file_name ASC"
@@ -473,8 +473,8 @@ if (isset($_GET['contact_id'])) {
<i class="fas fa-fw fa-copy mr-2"></i>Copy
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item"
href="post.php?unlink_asset_from_contact&contact_id=<?php echo $contact_id; ?>&asset_id=<?php echo $asset_id; ?>"
<a class="dropdown-item"
href="post.php?unlink_asset_from_contact&contact_id=<?php echo $contact_id; ?>&asset_id=<?php echo $asset_id; ?>"
class="btn btn-secondary btn-sm" title="Unlink">
<i class="fas fa-fw fa-unlink mr-2"></i>Unlink
</a>
@@ -613,8 +613,8 @@ if (isset($_GET['contact_id'])) {
<i class="fas fa-fw fa-share-alt mr-2"></i>Share
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item"
href="post.php?unlink_credential_from_contact&contact_id=<?php echo $contact_id; ?>&credential_id=<?php echo $credential_id; ?>"
<a class="dropdown-item"
href="post.php?unlink_credential_from_contact&contact_id=<?php echo $contact_id; ?>&credential_id=<?php echo $credential_id; ?>"
class="btn btn-secondary btn-sm" title="Unlink">
<i class="fas fa-fw fa-unlink mr-2"></i>Unlink
</a>
@@ -797,7 +797,9 @@ if (isset($_GET['contact_id'])) {
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-life-ring mr-2"></i>Related Tickets</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addTicketModal">
<button type="button" class="btn btn-primary ajax-modal"
data-modal-url="modals/ticket/ticket_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>"
data-modal-size="lg">
<i class="fas fa-plus mr-2"></i>New Ticket
</button>
</div>
@@ -1071,7 +1073,7 @@ if (isset($_GET['contact_id'])) {
</div>
</div>
</div>
<div class="card card-dark <?php if ($note_count == 0) { echo "d-none"; } ?>">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-sticky-note mr-2"></i>Notes</h3>

View File

@@ -152,8 +152,6 @@ ob_start();
</button>
</div>
</div>
</div>
<?php if ($client_id) { ?>

View File

@@ -87,13 +87,13 @@ ob_start();
<option value="0">- Choose a Template -</option>
<?php
$sql_ticket_templates = mysqli_query($mysqli, "
SELECT tt.ticket_template_id,
SELECT tt.ticket_template_id,
tt.ticket_template_name,
tt.ticket_template_subject,
tt.ticket_template_subject,
tt.ticket_template_details,
COUNT(ttt.task_template_id) as task_count
FROM ticket_templates tt
LEFT JOIN task_templates ttt
LEFT JOIN task_templates ttt
ON tt.ticket_template_id = ttt.task_template_ticket_template_id
WHERE tt.ticket_template_archived_at IS NULL
GROUP BY tt.ticket_template_id
@@ -463,32 +463,25 @@ ob_start();
</div>
</form>
<!-- Ticket Templates -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var templateSelect = $('#ticket_template_select');
var subjectInput = document.getElementById('subjectInput');
var detailsInput = document.getElementById('detailsInput');
$(document).on('change', '#ticket_template_select', function () {
const $opt = $(this).find(':selected');
const templateSubject = $opt.data('subject') || '';
const templateDetails = $opt.data('details') || '';
templateSelect.on('select2:select', function(e) {
var selectedOption = e.params.data.element;
var templateSubject = selectedOption.getAttribute('data-subject');
var templateDetails = selectedOption.getAttribute('data-details');
$('#subjectInput').val(templateSubject);
// Update Subject
subjectInput.value = templateSubject || '';
// Update Details
if (typeof tinymce !== 'undefined') {
var editor = tinymce.get('detailsInput');
if (editor) {
editor.setContent(templateDetails || '');
} else {
detailsInput.value = templateDetails || '';
}
if (window.tinymce) {
const editor = tinymce.get('detailsInput');
if (editor) {
editor.setContent(templateDetails);
} else {
detailsInput.value = templateDetails || '';
$('#detailsInput').val(templateDetails);
}
});
} else {
$('#detailsInput').val(templateDetails);
}
});
</script>

View File

@@ -18,7 +18,7 @@ ob_start();
<form action="post.php" method="post" autocomplete="off">
<!-- Hidden/System fields -->
<?php if ($client_id) { ?>
<input type="hidden" name="client" value="<?php echo $client_id; ?>>">
<input type="hidden" name="client" value="<?php echo $client_id; ?>">
<?php } ?>
<?php if ($project_id) { ?>
<input type="hidden" name="project" value="<?php echo $project_id; ?>">
@@ -58,13 +58,13 @@ ob_start();
<option value="0">- Choose a Template -</option>
<?php
$sql_ticket_templates = mysqli_query($mysqli, "
SELECT tt.ticket_template_id,
SELECT tt.ticket_template_id,
tt.ticket_template_name,
tt.ticket_template_subject,
tt.ticket_template_subject,
tt.ticket_template_details,
COUNT(ttt.task_template_id) as task_count
FROM ticket_templates tt
LEFT JOIN task_templates ttt
LEFT JOIN task_templates ttt
ON tt.ticket_template_id = ttt.task_template_ticket_template_id
WHERE tt.ticket_template_archived_at IS NULL
GROUP BY tt.ticket_template_id
@@ -189,7 +189,7 @@ ob_start();
<!-- Ticket client/contact -->
<?php if ($contact_id) { ?>
<input type="hidden" name="contact" value="<?php echo $contact_id; ?>">
<?php } else { ?>
<?php } else { ?>
<div class="tab-pane fade" id="pills-add-contacts">
<div class="form-group">
@@ -297,32 +297,24 @@ ob_start();
<!-- Ticket Templates -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var templateSelect = $('#ticket_template_select');
var subjectInput = document.getElementById('subjectInput');
var detailsInput = document.getElementById('detailsInput');
$(document).on('change', '#ticket_template_select', function () {
const $opt = $(this).find(':selected');
const templateSubject = $opt.data('subject') || '';
const templateDetails = $opt.data('details') || '';
templateSelect.on('select2:select', function(e) {
var selectedOption = e.params.data.element;
var templateSubject = selectedOption.getAttribute('data-subject');
var templateDetails = selectedOption.getAttribute('data-details');
$('#subjectInput').val(templateSubject);
// Update Subject
subjectInput.value = templateSubject || '';
// Update Details
if (typeof tinymce !== 'undefined') {
var editor = tinymce.get('detailsInput');
if (editor) {
editor.setContent(templateDetails || '');
} else {
detailsInput.value = templateDetails || '';
}
} else {
detailsInput.value = templateDetails || '';
}
});
});
if (window.tinymce) {
const editor = tinymce.get('detailsInput');
if (editor) {
editor.setContent(templateDetails);
} else {
$('#detailsInput').val(templateDetails);
}
} else {
$('#detailsInput').val(templateDetails);
}
});
</script>
<!-- Ticket Client/Contact JS -->

View File

@@ -0,0 +1,140 @@
<?php
require_once '../../../includes/modal_header.php';
$task_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM tasks
WHERE task_id = $task_id
LIMIT 1"
);
$row = mysqli_fetch_array($sql);
$task_name = nullable_htmlentities($row['task_name']);
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-shield-alt mr-2"></i>New approver for task <?=$task_name?></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="task_id" value="<?php echo $task_id; ?>">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="modal-body">
<div class="form-group">
<label>Approval scope <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-layer-group"></i></span>
</div>
<select class="form-control" name="approval_scope" id="approval_scope" required>
<option value="">Select scope...</option>
<option value="internal">Internal</option>
<option value="client">Client</option>
</select>
</div>
</div>
<div class="form-group d-none" id="approval_type_wrapper">
<label>Who can approve? <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user-check"></i></span>
</div>
<select class="form-control" name="approval_type" id="approval_type" required>
<!-- JS -->
</select>
</div>
</div>
<div class="form-group d-none" id="specific_user_wrapper">
<label>Select specific internal approver <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-user-circle"></i></span>
</div>
<select class="form-control select2" name="approval_required_user_id" id="specific_user_select">
<option value="">Select user...</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_ticket_task_approver" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div>
</form>
<!-- JS to make the correct boxes appear depending on if internal/client approval) -->
<script>
$('#approval_scope').on('change', function() {
const scope = $(this).val();
const typeSelect = $('#approval_type');
const wrapper = $('#approval_type_wrapper');
typeSelect.empty();
$('#specific_user_wrapper').addClass('d-none');
if (!scope) {
wrapper.addClass('d-none');
return;
}
wrapper.removeClass('d-none');
if (scope === 'internal') {
typeSelect.append('<option value="">Select...</option>');
typeSelect.append('<option value="any">Any internal reviewer</option>');
typeSelect.append('<option value="specific">Specific agent</option>');
}
if (scope === 'client') {
typeSelect.append('<option value="">Select...</option>');
typeSelect.append('<option value="any">Ticket contact</option>');
typeSelect.append('<option value="technical">Technical contacts</option>');
typeSelect.append('<option value="billing">Billing contacts</option>');
}
});
// Specific user (internal only for now)
$('#approval_type').on('change', function() {
const type = $(this).val();
const scope = $('#approval_scope').val();
const userSelect = $('#specific_user_select');
if (type !== 'specific' || scope !== 'internal') {
$('#specific_user_wrapper').addClass('d-none');
return;
}
$('#specific_user_wrapper').removeClass('d-none');
userSelect.empty().append('<option value="">Loading...</option>');
$.getJSON('ajax.php?get_internal_users=true', function(data) {
userSelect.empty().append('<option value="">Select user...</option>');
data.users.forEach(function(u) {
userSelect.append(`<option value="${u.user_id}">${u.user_name}</option>`);
});
});
});
</script>
<?php
require_once '../../../includes/modal_footer.php';

View File

@@ -14,6 +14,14 @@ $task_name = nullable_htmlentities($row['task_name']);
$task_completion_estimate = intval($row['task_completion_estimate']);
$task_completed_at = nullable_htmlentities($row['task_completed_at']);
// Approvals
$sql_task_approvals = mysqli_query($mysqli, "
SELECT user_name, approval_id, approval_scope, approval_type, approval_required_user_id, approval_status, approval_created_by, approval_approved_by FROM task_approvals
LEFT JOIN users ON user_id = approval_required_user_id
WHERE approval_task_id = $task_id
ORDER BY approval_approved_by"
);
// Generate the HTML form content using output buffering.
ob_start();
@@ -27,7 +35,7 @@ ob_start();
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="task_id" value="<?php echo $task_id; ?>">
<div class="modal-body">
<div class="form-group">
@@ -49,7 +57,53 @@ ob_start();
<input type="number" class="form-control" name="completion_estimate" placeholder="Estimated time to complete task in mins" value="<?php echo $task_completion_estimate; ?>">
</div>
</div>
<?php if (mysqli_num_rows($sql_task_approvals) > 0) { ?>
<hr>
<div class="form-group">
<b>Task Approvals</b>
<table class="table table-sm table-bordered" style="margin-top:10px;">
<thead>
<tr>
<th>Scope</th>
<th>Type</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_array($sql_task_approvals)) {
$approval_id = intval($row['approval_id']);
$approval_scope = nullable_htmlentities($row['approval_scope']);
$approval_type = nullable_htmlentities($row['approval_type']);
$approval_user_name = nullable_htmlentities($row['user_name']);
$approval_status = nullable_htmlentities($row['approval_status']);
$approval_created_by = intval($row['approval_created_by']);
$approval_approved_by = nullable_htmlentities($row['approval_approved_by']);
?>
<tr>
<td><?= ucfirst($approval_scope) ?></td>
<td><?= ucfirst($approval_type) ?> <?php if (!empty($approval_user_name)) { echo " - $approval_user_name"; } ?></td>
<td><?= ucfirst($approval_status) ?></td>
<td>
<?php if ($approval_status !== 'approved') { ?>
<a class="text-danger"
onclick="return confirm('Delete this approval request?');"
href="post.php?delete_ticket_task_approver=<?= $approval_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash-alt"></i>Delete
</a>
<!-- confirm-link won't work -->
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } ?>
</div>
<div class="modal-footer">

View File

@@ -794,7 +794,7 @@ if (isset($_POST['bulk_add_client_ticket'])) {
$config_base_url = sanitizeInput($config_base_url);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_category = $category_id, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_status = $ticket_status, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_url_key = '$url_key', ticket_client_id = $client_id, ticket_project_id = $project_id");

View File

@@ -29,7 +29,7 @@ if (isset($_POST['add_invoice'])) {
$invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $invoice_number, invoice_scope = '$scope', invoice_date = '$date', invoice_due = DATE_ADD('$date', INTERVAL $client_net_terms day), invoice_discount_amount = '$invoice_discount', invoice_amount = '$invoice_amount', invoice_currency_code = '$session_company_currency', invoice_category_id = $category, invoice_status = 'Draft', invoice_url_key = '$url_key', invoice_client_id = $client_id");
@@ -112,7 +112,7 @@ if (isset($_POST['add_invoice_copy'])) {
$new_invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$invoice_scope', invoice_date = '$date', invoice_due = DATE_ADD('$date', INTERVAL $client_net_terms day), invoice_category_id = $category_id, invoice_status = 'Draft', invoice_discount_amount = $invoice_discount_amount, invoice_amount = $invoice_amount, invoice_currency_code = '$invoice_currency_code', invoice_note = '$invoice_note', invoice_url_key = '$url_key', invoice_client_id = $client_id");
@@ -570,15 +570,13 @@ if (isset($_GET['email_invoice'])) {
}
// Queue Mail
$data = [
[
$data[] = [
'from' => $config_invoice_from_email,
'from_name' => $config_invoice_from_name,
'recipient' => $contact_email,
'recipient_name' => $contact_name,
'subject' => $subject,
'body' => $body
]
];
addToMailQueue($data);
@@ -613,15 +611,13 @@ if (isset($_GET['email_invoice'])) {
$billing_contact_name = sanitizeInput($billing_contact['contact_name']);
$billing_contact_email = sanitizeInput($billing_contact['contact_email']);
$data = [
[
$data[] = [
'from' => $config_invoice_from_email,
'from_name' => $config_invoice_from_name,
'recipient' => $billing_contact_email,
'recipient_name' => $billing_contact_name,
'subject' => $subject,
'body' => $body
]
];
logAction("Invoice", "Email", "$session_name Emailed $billing_contact_email Invoice $invoice_prefix$invoice_number Email queued Email ID: $email_id", $client_id, $invoice_id);
@@ -659,7 +655,7 @@ if (isset($_POST['export_invoices_csv'])) {
$file_name_date = date('Y-m-d_H-i-s');
}
$sql = mysqli_query($mysqli,"SELECT * FROM invoices LEFT JOIN clients ON invoice_client_id = client_id WHERE $date_query $client_query ORDER BY invoice_number ASC");
$sql = mysqli_query($mysqli,"SELECT * FROM invoices LEFT JOIN clients ON invoice_client_id = client_id WHERE $date_query AND $client_query ORDER BY invoice_number ASC");
$num_rows = mysqli_num_rows($sql);

View File

@@ -26,7 +26,7 @@ if (isset($_POST['add_quote'])) {
$quote_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access
$quote_url_key = randomString(156);
$quote_url_key = randomString(32);
mysqli_query($mysqli,"INSERT INTO quotes SET quote_prefix = '$config_quote_prefix', quote_number = $quote_number, quote_scope = '$scope', quote_date = '$date', quote_expire = '$expire', quote_currency_code = '$session_company_currency', quote_category_id = $category, quote_status = 'Draft', quote_url_key = '$quote_url_key', quote_client_id = $client_id");
@@ -78,7 +78,7 @@ if (isset($_POST['add_quote_copy'])) {
$category_id = intval($row['quote_category_id']);
//Generate a unique URL key for clients to access
$quote_url_key = randomString(156);
$quote_url_key = randomString(32);
mysqli_query($mysqli,"INSERT INTO quotes SET quote_prefix = '$config_quote_prefix', quote_number = $quote_number, quote_scope = '$quote_scope', quote_date = '$date', quote_expire = '$expire', quote_category_id = $category_id, quote_status = 'Draft', quote_discount_amount = $quote_discount_amount, quote_amount = $quote_amount, quote_currency_code = '$quote_currency_code', quote_note = '$quote_note', quote_url_key = '$quote_url_key', quote_client_id = $client_id");
@@ -147,7 +147,7 @@ if (isset($_POST['add_quote_to_invoice'])) {
$invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $invoice_number, invoice_scope = '$quote_scope', invoice_date = '$date', invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_category_id = $category_id, invoice_status = 'Draft', invoice_discount_amount = $quote_discount_amount, invoice_amount = $quote_amount, invoice_currency_code = '$quote_currency_code', invoice_note = '$quote_note', invoice_url_key = '$url_key', invoice_client_id = $client_id");

View File

@@ -310,7 +310,7 @@ if (isset($_GET['force_recurring'])) {
$new_invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Sent', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id");

View File

@@ -90,7 +90,7 @@ if (isset($_POST['bulk_force_recurring_tickets'])) {
$client_id = intval($row['recurring_ticket_client_id']);
$asset_id = intval($row['recurring_ticket_asset_id']);
$category = intval($row['recurring_ticket_category']);
$url_key = randomString(156);
$url_key = randomString(32);
$ticket_status = 1; // Default
if ($assigned_id > 0) {
@@ -228,7 +228,7 @@ if (isset($_GET['force_recurring_ticket'])) {
$client_id = intval($row['recurring_ticket_client_id']);
$asset_id = intval($row['recurring_ticket_asset_id']);
$category = intval($row['recurring_ticket_category']);
$url_key = randomString(156);
$url_key = randomString(32);
$ticket_status = 1; // Default
if ($assigned_id > 0) {

View File

@@ -155,6 +155,247 @@ if (isset($_GET['undo_complete_task'])) {
}
if (isset($_POST['add_ticket_task_approver'])) {
validateCSRFToken($_POST['csrf_token']);
enforceUserPermission('module_support', 2);
$task_id = intval($_POST['task_id']);
$scope = sanitizeInput($_POST['approval_scope']);
$type = sanitizeInput($_POST['approval_type']);
$approval_url_key = randomString(32);
$required_user_id = "NULL";
if ($type == 'specific') {
$required_user_id = intval($_POST['approval_required_user_id']);
}
mysqli_query($mysqli, "INSERT INTO task_approvals SET approval_scope = '$scope', approval_type = '$type', approval_required_user_id = $required_user_id, approval_status = 'pending', approval_created_by = $session_user_id, approval_url_key = '$approval_url_key', approval_task_id = $task_id");
$approval_id = mysqli_insert_id($mysqli);
// Task/Ticket Info
$tt_row = mysqli_fetch_array(mysqli_query($mysqli, "
SELECT * FROM tasks
LEFT JOIN tickets ON ticket_id = task_ticket_id
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
WHERE task_id = $task_id LIMIT 1
")
);
$task_name = sanitizeInput($tt_row['task_name']);
$ticket_id = intval($tt_row['task_ticket_id']);
$ticket_prefix = sanitizeInput($tt_row['ticket_prefix']);
$ticket_number = intval($tt_row['ticket_number']);
$ticket_subject = sanitizeInput($tt_row['ticket_subject']);
$ticket_status = sanitizeInput($tt_row['ticket_status_name']);
$ticket_url_key = sanitizeInput($tt_row['ticket_url_key']);
$ticket_contact_id = intval($tt_row['ticket_contact_id']);
$client_id = intval($tt_row['ticket_client_id']);
// --Notifications--
// Sanitize Config vars from get_settings.php
$config_ticket_from_name = sanitizeInput($config_ticket_from_name);
$config_ticket_from_email = sanitizeInput($config_ticket_from_email);
$config_base_url = sanitizeInput($config_base_url);
// Get Company Info
$crow = mysqli_fetch_array(mysqli_query($mysqli, "SELECT company_name, company_phone, company_phone_country_code FROM companies WHERE company_id = 1"));
$company_name = sanitizeInput($crow['company_name']);
$company_phone = sanitizeInput(formatPhoneNumber($crow['company_phone'], $crow['company_phone_country_code']));
// Email contents
$subject = "Ticket task approval required - [$ticket_prefix$ticket_number] - $ticket_subject";
$body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello,<br><br>A ticket regarding $ticket_subject has a task requiring your approval:- <br>Task name: $task_name<br>Scope/Type: $scope - $type <br><br>To approve this task, please click <a href=\'https://$config_base_url/guest/guest_approve_ticket_task.php?task_approval_id=$approval_id&url_key=$approval_url_key\'>here</a>.<br>If you require further information, please reply to this e-mail.<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: $ticket_status<br>Portal: <a href=\'https://$config_base_url/guest/guest_view_ticket.php?ticket_id=$ticket_id&url_key=$ticket_url_key\'>View ticket</a><br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
if ($scope == 'internal' && $type == 'specific' && $session_user_id !== $required_user_id) {
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$session_name needs your approval for ticket $ticket_prefix$ticket_number task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_client_id = 0, notification_user_id = $required_user_id");
if (!empty($config_smtp_host)) {
$agent_contact = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_name, user_email FROM users WHERE user_id = $required_user_id AND user_archived_at IS NULL"));
$name = sanitizeInput($agent_contact['user_name']);
$email = sanitizeInput($agent_contact['user_email']);
// Only add contact to email queue if email is valid
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
$data[] = [
'from' => $config_ticket_from_email,
'from_name' => $config_ticket_from_name,
'recipient' => $email,
'recipient_name' => $name,
'subject' => $subject,
'body' => $body
];
addToMailQueue($data);
}
}
}
if (!empty($config_smtp_host) && $scope == 'client' && $type == 'any') {
$contact_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name, contact_email FROM contacts WHERE contact_id = $ticket_contact_id LIMIT 1"));
$contact_name = sanitizeInput($contact_row['contact_name']);
$contact_email = sanitizeInput($contact_row['contact_email']);
$data = [];
if (filter_var($contact_email, FILTER_VALIDATE_EMAIL)) {
$data[] = [
'from' => $config_ticket_from_email,
'from_name' => $config_ticket_from_name,
'recipient' => $contact_email,
'recipient_name' => $contact_name,
'subject' => $subject,
'body' => $body
];
addToMailQueue($data);
}
}
if (!empty($config_smtp_host) && $scope == 'client' && $type == 'technical') {
$sql_technical_contacts = mysqli_query(
$mysqli,
"SELECT contact_name, contact_email FROM contacts
WHERE contact_technical = 1
AND contact_email != ''
AND contact_client_id = $client_id"
);
$data = [];
while ($technical_contact = mysqli_fetch_array($sql_technical_contacts)) {
$technical_contact_name = sanitizeInput($technical_contact['contact_name']);
$technical_contact_email = sanitizeInput($technical_contact['contact_email']);
if (filter_var($technical_contact_email, FILTER_VALIDATE_EMAIL)) {
$data[] = [
'from' => $config_ticket_from_email,
'from_name' => $config_ticket_from_name,
'recipient' => $technical_contact_email,
'recipient_name' => $technical_contact_name,
'subject' => $subject,
'body' => $body
];
}
}
addToMailQueue($data);
}
if (!empty($config_smtp_host) && $scope == 'client' && $type == 'billing') {
$sql_billing_contacts = mysqli_query(
$mysqli,
"SELECT contact_name, contact_email FROM contacts
WHERE contact_billing = 1
AND contact_email != ''
AND contact_client_id = $client_id"
);
$data = [];
while ($billing_contact = mysqli_fetch_array($sql_billing_contacts)) {
$billing_contact_name = sanitizeInput($billing_contact['contact_name']);
$billing_contact_email = sanitizeInput($billing_contact['contact_email']);
if (filter_var($billing_contact_email, FILTER_VALIDATE_EMAIL)) {
$data[] = [
'from' => $config_ticket_from_email,
'from_name' => $config_ticket_from_name,
'recipient' => $billing_contact_email,
'recipient_name' => $billing_contact_name,
'subject' => $subject,
'body' => $body
];
}
}
addToMailQueue($data);
}
// Logging
logAction("Task", "Edit", "$session_name added task approver for $task_name", $client_id, $task_id);
flash_alert("Added approver");
redirect();
}
if (isset($_GET['approve_ticket_task'])) {
validateCSRFToken($_GET['csrf_token']);
enforceUserPermission('module_support', 2);
$task_id = intval($_GET['approve_task']);
$approval_id = intval($_GET['approval_id']);
$approval_row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_scope = 'internal'"));
$task_name = nullable_htmlentities($approval_row['task_name']);
$scope = nullable_htmlentities($approval_row['approval_scope']);
$type = nullable_htmlentities($approval_row['approval_type']);
$required_user = intval($approval_row['approval_required_user_id']);
$created_by = intval($approval_row['approval_created_by']);
$ticket_id = intval($approval_row['task_ticket_id']);
if (!$approval_row) {
flash_alert("Cannot find/approve that task", 'error');
redirect();
exit;
}
// Validate approver (deny)
if ($required_user > 0 && $required_user !== $session_user_id) {
flash_alert("You cannot approve that task", 'error');
redirect();
exit;
}
if ($required_user == 0 && $type == 'any' && $created_by == $session_user_id) {
flash_alert("You cannot approve your own task", 'error');
redirect();
exit;
}
// Approve
mysqli_query($mysqli, "UPDATE task_approvals SET approval_status = 'approved', approval_approved_by = $session_user_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_scope = 'internal'");
// Notify
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$session_name approved ticket task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_client_id = 0, notification_user_id = $created_by");
// TODO: Email agent
// Logging
logAction("Task", "Edit", "$session_name approved task $task_name (approval $approval_id)", 0, $task_id);
flash_alert("Approved");
redirect();
}
if (isset($_GET['delete_ticket_task_approver'])) {
validateCSRFToken($_GET['csrf_token']);
enforceUserPermission('module_support', 3);
$approval_id = intval($_GET['delete_ticket_task_approver']);
mysqli_query($mysqli, "DELETE FROM task_approvals WHERE approval_id = $approval_id");
logAction("Task", "Delete", "$session_name deleted task approval request ($approval_id)", 0, 0);
flash_alert("Approval request deleted", 'error');
redirect();
}
if (isset($_GET['complete_all_tasks'])) {
enforceUserPermission('module_support', 2);

View File

@@ -68,7 +68,7 @@ if (isset($_POST['add_ticket'])) {
$config_base_url = sanitizeInput($config_base_url);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Agent', ticket_category = $category_id, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = '$billable', ticket_status = '$ticket_status', ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_location_id = $location_id, ticket_asset_id = $asset_id, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_url_key = '$url_key', ticket_due_at = $due, ticket_client_id = $client_id, ticket_invoice_id = 0, ticket_project_id = $project_id");
@@ -1521,7 +1521,7 @@ if (isset($_POST['bulk_add_asset_ticket'])) {
$config_base_url = sanitizeInput($config_base_url);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_category = $category_id, ticket_subject = '$subject_asset_prepended', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_status = $ticket_status, ticket_asset_id = $asset_id, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_url_key = '$url_key', ticket_client_id = $client_id, ticket_project_id = $project_id");
@@ -2167,7 +2167,7 @@ if (isset($_POST['add_invoice_from_ticket'])) {
$invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli, "INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $invoice_number, invoice_scope = '$scope', invoice_date = '$date', invoice_due = DATE_ADD('$date', INTERVAL $client_net_terms day), invoice_currency_code = '$session_company_currency', invoice_category_id = $category, invoice_status = 'Draft', invoice_url_key = '$url_key', invoice_client_id = $client_id");
$invoice_id = mysqli_insert_id($mysqli);

View File

@@ -222,7 +222,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</form>
<?php } else { ?>
No Cards on File
<?php } ?>
<?php } ?>
</td>
<td>
<span class="p-2 badge badge-<?php echo $status_badge_color; ?>">

View File

@@ -65,7 +65,7 @@ $sql = mysqli_query(
$billable_query
$client_query
ORDER BY
CASE
CASE
WHEN '$sort' = 'recurring_ticket_priority' THEN
CASE recurring_ticket_priority
WHEN 'High' THEN 1
@@ -74,7 +74,7 @@ $sql = mysqli_query(
ELSE 4 -- Optional: for unexpected priority values
END
ELSE NULL
END $order,
END $order,
$sort $order -- Apply normal sorting by $sort and $order
LIMIT $record_from, $record_to"
);
@@ -152,7 +152,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="col-sm-2">
<div class="form-group">
<select class="form-control select2" name="billable" onchange="this.form.submit()">
<option value="">- Billable Status -</option>
<option value="">- Billable Status -</option>
<option <?php if ($billable_filter == 1) { echo "selected"; } ?> value="1">Billable</option>
<option <?php if ($billable_filter == 0) { echo "selected"; } ?> value="0">Non-Billable</option>
</select>
@@ -170,31 +170,31 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<i class="fas fa-fw fa-paper-plane mr-2"></i>Force Reoccur
</button>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#"
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/recurring_ticket/recurring_ticket_bulk_agent_edit.php"
data-bulk="true">
<i class="fas fa-fw fa-user-check mr-2"></i>Assign Agent
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#"
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/recurring_ticket/recurring_ticket_bulk_category_edit.php"
data-bulk="true">
<i class="fas fa-fw fa-layer-group mr-2"></i>Set Category
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#"
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/recurring_ticket/recurring_ticket_bulk_priority_edit.php"
data-bulk="true">
<i class="fas fa-fw fa-thermometer-half mr-2"></i>Set Priority
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#"
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/recurring_ticket/recurring_ticket_bulk_billable_edit.php"
data-bulk="true">
<i class="fas fa-fw fa-dollar-sign mr-2"></i>Set Billable
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#"
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/recurring_ticket/recurring_ticket_bulk_next_run_edit.php"
data-bulk="true">
<i class="fas fa-fw fa-calendar-day mr-2"></i>Set Next Run Date
@@ -260,7 +260,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
Agent <?php if ($sort == 'user_name') { echo $order_icon; } ?>
</a>
</th>
<?php if (!$client_url) { ?>
<th>
<a class="text-secondary" href="?<?php echo $url_query_strings_sort; ?>&sort=client_name&order=<?php echo $disp; ?>">

View File

@@ -961,23 +961,82 @@ if (isset($_GET['ticket_id'])) {
<table class="table table-sm" id="tasks">
<?php
while($row = mysqli_fetch_array($sql_tasks)){
while ($row = mysqli_fetch_array($sql_tasks)) {
$task_id = intval($row['task_id']);
$task_name = nullable_htmlentities($row['task_name']);
//$task_description = nullable_htmlentities($row['task_description']); // not in db yet
$task_completion_estimate = intval($row['task_completion_estimate']);
$task_completed_at = nullable_htmlentities($row['task_completed_at']);
// Check for approvals
$task_needs_approval = false;
$task_needs_approval = mysqli_num_rows(mysqli_query(
$mysqli,
"SELECT 1 FROM task_approvals
WHERE approval_task_id = $task_id
AND approval_status IN ('pending','declined')
LIMIT 1"
)) > 0;
$approval_id = 0;
$user_can_approve = false;
$approval_rows = mysqli_query($mysqli, "
SELECT approval_id, approval_scope, approval_type, approval_required_user_id, approval_created_by
FROM task_approvals WHERE approval_task_id = $task_id AND approval_status = 'pending'
");
while ($approval = mysqli_fetch_array($approval_rows)) {
$scope = nullable_htmlentities($approval['approval_scope']);
$type = nullable_htmlentities($approval['approval_type']);
$required_user = intval($approval['approval_required_user_id']);
$created_by = intval($approval['approval_created_by']);
// Named, specific user?
if ($scope == 'internal' && $type == 'specific' && $required_user == $session_user_id) {
$user_can_approve = true;
$approval_id = intval($approval['approval_id']);
continue;
}
// Any internal user, but the one who created the task
if ($scope == 'internal' && $type == 'any' && $created_by !== $session_user_id) {
$user_can_approve = true;
$approval_id = intval($approval['approval_id']);
continue;
}
}
?>
<tr data-task-id="<?= $task_id ?>">
<td>
<?php if ($task_completed_at) { ?>
<i class="far fa-check-square text-success"></i>
<?php } elseif (lookupUserPermission("module_support") >= 2) { ?>
<a href="post.php?complete_task=<?php echo $task_id; ?>">
<i class="far fa-square text-dark"></i>
</a>
<?php if ($task_needs_approval) { ?>
<i class="fas fa-shield-alt text-warning"
data-toggle="tooltip"
data-placement="top"
title="Approval required"></i>
<?php if ($user_can_approve) { ?>
<a class="confirm-link" href="post.php?approve_ticket_task=<?= $task_id ?>&approval_id=<?= $approval_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-thumbs-up text-green"></i>
</a>
<?php } ?>
<span class="text-dark ml-2"><?= $task_name ?></span>
<?php } else { ?>
<a href="post.php?complete_task=<?php echo $task_id; ?>">
<i class="far fa-square text-dark"></i>
</a>
<span class="text-dark ml-2"><?php echo $task_name; ?></span>
<?php } ?>
<?php } ?>
<span class="text-dark ml-2"><?php echo $task_name; ?></span>
</td>
<td>
<div class="float-right">
@@ -997,6 +1056,12 @@ if (isset($_GET['ticket_id'])) {
data-modal-url="modals/ticket/ticket_task_edit.php?id=<?= $task_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<?php if (!$task_completed_at) { ?>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/ticket/ticket_task_approver_add.php?id=<?= $task_id ?>">
<i class="fas fa-fw fa-shield-alt mr-2"></i>Add Approvers
</a>
<?php } ?>
<?php if ($task_completed_at) { ?>
<a class="dropdown-item" href="post.php?undo_complete_task=<?php echo $task_id; ?>">
<i class="fas fa-fw fa-arrow-circle-left mr-2"></i>Mark incomplete

View File

@@ -7,7 +7,7 @@
<table class="table table-striped table-borderless table-hover">
<thead class="text-dark <?php if (!$num_rows[0]) { echo "d-none"; } ?> text-nowrap">
<tr>
<td>
<?php if ($status !== 'Closed') { ?>
<div class="form-check">
@@ -26,7 +26,7 @@
Subject <?php if ($sort == 'ticket_subject') { echo $order_icon; } ?>
</a>
</th>
<th>
<?php if (!$client_url) { ?>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=client_name&order=<?php echo $disp; ?>">
@@ -39,7 +39,7 @@
</th>
<?php if ($config_module_enable_accounting && lookupUserPermission("module_sales") >= 2) { ?>
<th class="text-center">
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ticket_billable&order=<?php echo $disp; ?>">
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ticket_billable&order=<?= $disp ?>">
Billable <?php if ($sort == 'ticket_billable') { echo $order_icon; } ?>
</a>
</th>
@@ -186,7 +186,7 @@
if($task_count) {
$tasks_completed_percent = round(($completed_task_count / $task_count) * 100);
}
?>
<tr class="<?php if(empty($ticket_closed_at) && empty($ticket_updated_at)) { echo "text-bold"; }?> <?php if (empty($ticket_closed_at) && $ticket_reply_type == "Client") { echo "table-warning"; } ?>">
@@ -199,7 +199,7 @@
</div>
<?php } ?>
</td>
<!-- Ticket Number -->
<td>
<a href="ticket.php?ticket_id=<?= "$ticket_id$has_client" ?>">
@@ -242,9 +242,9 @@
data-modal-url="modals/ticket/ticket_billable.php?id=<?= $ticket_id ?>">
<?php
if ($ticket_billable == 1) {
echo "<span class='badge badge-pill badge-success p-2'>Yes</span>";
echo "<span class='badge badge-pill badge-success p-2'><i class='fas fa-fw fa-check'></i></span>";
} else {
echo "<span class='badge badge-pill badge-secondary p-2'>No</span>";
echo "<span class='badge badge-pill badge-secondary p-2'><i class='fas fa-fw fa-minus'></i></span>";
}
?>
</a>
@@ -303,7 +303,7 @@
<?php
}
?>
</tbody>

View File

@@ -25,7 +25,7 @@ $data = "otpauth://totp/ITFlow:$session_email?secret=$token";
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<div class="modal-body">
<div class="text-center">
<img src='../../plugins/barcode/barcode.php?f=png&s=qr&d=<?php echo $data; ?>'>
<p><span class='text-secondary'>Secret:</span> <?php echo $token; ?>
@@ -51,4 +51,3 @@ $data = "otpauth://totp/ITFlow:$session_email?secret=$token";
</div>
</div>
</div>

View File

@@ -88,7 +88,7 @@ if (isset($_POST['edit_your_user_details'])) {
}
if (isset($_GET['clear_your_user_avatar'])) {
validateCSRFToken($_GET['csrf_token']);
mysqli_query($mysqli,"UPDATE users SET user_avatar = NULL WHERE user_id = $session_user_id");
@@ -96,7 +96,7 @@ if (isset($_GET['clear_your_user_avatar'])) {
logAction("User Account", "Edit", "$session_name cleared their avatar");
flash_alert("Avatar cleared", 'error');
redirect();
}
@@ -167,7 +167,7 @@ if (isset($_POST['edit_your_user_preferences'])) {
// Enable extension access, only if it isn't already setup (user doesn't have cookie)
if (isset($_POST['extension']) && $_POST['extension'] == 'Yes') {
if (!isset($_COOKIE['user_extension_key'])) {
$extension_key = randomString(156);
$extension_key = randomString(32);
mysqli_query($mysqli, "UPDATE users SET user_extension_key = '$extension_key' WHERE user_id = $session_user_id");
$extended_log_description .= "enabled browser extension access";
@@ -196,7 +196,7 @@ if (isset($_POST['enable_mfa'])) {
require_once "../../plugins/totp/totp.php";
// Grab the code from the user
$verify_code = trim($_POST['verify_code']);
$verify_code = trim($_POST['verify_code']);
// Ensure it's numeric
if (!ctype_digit($verify_code)) {
$verify_code = '';
@@ -227,9 +227,9 @@ if (isset($_POST['enable_mfa'])) {
if ($previousPage === 'mfa_enforcement.php') {
// Redirect back to mfa_enforcement.php
redirect("../$config_start_page");
}
}
}
} else {
// FAILURE
@@ -245,7 +245,7 @@ if (isset($_POST['enable_mfa'])) {
// Redirect back to mfa_enforcement.php
redirect();
}
}
}
}
redirect("user_security.php");

View File

@@ -44,7 +44,7 @@ if (!empty($subject)) {
$ticket_number = mysqli_insert_id($mysqli);
// Insert ticket
$url_key = randomString(156);
$url_key = randomString(32);
$insert_sql = mysqli_query($mysqli,"INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'API', ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $billable, ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_created_by = 0, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_asset_id = $asset, ticket_url_key = '$url_key', ticket_client_id = $client_id");
// Check insert & get insert ID

View File

@@ -16,13 +16,13 @@ if (!isset($_SESSION)) {
}
if (!isset($_SESSION['client_logged_in']) || !$_SESSION['client_logged_in']) {
header("Location: /client/login.php");
header("Location: /login.php");
die;
}
// Check user type
if ($_SESSION['user_type'] !== 2) {
header("Location: /client/login.php");
header("Location: /login.php");
exit();
}

View File

@@ -1,234 +0,0 @@
<?php
/*
* Client Portal
* Landing / Home page for the client portal
*/
header("Content-Security-Policy: default-src 'self'");
require_once '../config.php';
require_once '../functions.php';
require_once '../includes/load_global_settings.php';
if (!isset($_SESSION)) {
// HTTP Only cookies
ini_set("session.cookie_httponly", true);
if ($config_https_only) {
// Tell client to only send cookie(s) over HTTPS
ini_set("session.cookie_secure", true);
}
session_start();
}
// Set Timezone after session_start
require_once "../includes/inc_set_timezone.php";
// Check to see if client portal is enabled
if($config_client_portal_enable == 0) {
echo "Client Portal is Disabled";
exit();
}
$session_ip = sanitizeInput(getIP());
$session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT']);
$sql_settings = mysqli_query($mysqli, "SELECT config_azure_client_id, config_login_message FROM settings WHERE company_id = 1");
$settings = mysqli_fetch_array($sql_settings);
$azure_client_id = $settings['config_azure_client_id'];
$config_login_message = nullable_htmlentities($settings['config_login_message']);
$company_sql = mysqli_query($mysqli, "SELECT company_name, company_logo FROM companies WHERE company_id = 1");
$company_results = mysqli_fetch_array($company_sql);
$company_name = $company_results['company_name'];
$company_logo = $company_results['company_logo'];
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['login'])) {
$email = sanitizeInput($_POST['email']);
$password = $_POST['password'];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
header("HTTP/1.1 401 Unauthorized");
$_SESSION['login_message'] = 'Invalid e-mail';
} else {
$sql = mysqli_query($mysqli, "SELECT * FROM users
LEFT JOIN contacts ON user_id = contact_user_id
LEFT JOIN clients ON contact_client_id = client_id
WHERE user_email = '$email'
AND client_archived_at IS NULL
AND user_archived_at IS NULL
AND user_type = 2
AND user_status = 1
LIMIT 1"
);
$row = mysqli_fetch_array($sql);
$client_id = intval($row['contact_client_id']);
$user_id = intval($row['user_id']);
$session_user_id = $user_id; // to pass the user_id to logAction function
$contact_id = intval($row['contact_id']);
$user_email = sanitizeInput($row['user_email']);
$user_auth_method = sanitizeInput($row['user_auth_method']);
if ($user_auth_method == 'local') {
if (password_verify($password, $row['user_password'])) {
$_SESSION['client_logged_in'] = true;
$_SESSION['client_id'] = $client_id;
$_SESSION['user_id'] = $user_id;
$_SESSION['user_type'] = 2;
$_SESSION['contact_id'] = $contact_id;
$_SESSION['login_method'] = "local";
header("Location: index.php");
// Logging
logAction("Client Login", "Success", "Client contact $user_email successfully logged in locally", $client_id, $user_id);
} else {
// Logging
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (incorrect password for contact ID $contact_id)", $client_id, $user_id);
header("HTTP/1.1 401 Unauthorized");
$_SESSION['login_message'] = 'Incorrect username or password.';
}
} else {
// Logging
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid email/not allowed local auth)");
header("HTTP/1.1 401 Unauthorized");
$_SESSION['login_message'] = 'Incorrect username or password.';
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title><?php echo $company_name; ?> | Client Portal Login</title>
<!-- Tell the browser to be responsive to screen width -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<!-- Favicon - If Fav Icon exists else use the default one -->
<?php if(file_exists('../uploads/favicon.ico')) { ?>
<link rel="icon" type="image/x-icon" href="../uploads/favicon.ico">
<?php } ?>
<!-- Font Awesome -->
<link rel="stylesheet" href="../plugins/fontawesome-free/css/all.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="../plugins/adminlte/css/adminlte.min.css">
</head>
<body class="hold-transition login-page">
<div class="login-box">
<div class="login-logo">
<?php if (!empty($company_logo)) { ?>
<img alt="<?=$company_name?> logo" height="110" width="380" class="img-fluid" src="<?php echo "../uploads/settings/$company_logo"; ?>">
<?php } else { ?>
<b><?=$company_name?></b> <br>Client Portal Login</h2>
<?php } ?>
</div>
<div class="card">
<div class="card-body login-card-body">
<?php if(!empty($config_login_message)){ ?>
<p class="login-box-msg px-0"><?php echo nl2br($config_login_message); ?></p>
<?php } ?>
<?php
if (!empty($_SESSION['login_message'])) { ?>
<p class="login-box-msg text-danger">
<?php
echo $_SESSION['login_message'];
unset($_SESSION['login_message']);
?>
</p>
<?php
}
?>
<form method="post">
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Registered Client Email" name="email" required autofocus>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
</div>
</div>
</div>
<div class="input-group mb-3">
<input type="password" class="form-control" placeholder="Client Password" name="password" required>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
<button type="submit" class="btn btn-success btn-block mb-3" name="login">Sign in</button>
<hr>
<?php
if (!empty($config_smtp_host)) { ?>
<h5 class="text-center"><a href="login_reset.php">Forgot password?</a></h5>
<?php } ?>
</form>
<?php
if (!empty($azure_client_id)) { ?>
<hr>
<div class="col text-center">
<a href="login_microsoft.php">
<button type="button" class="btn btn-secondary">Login with Microsoft Entra</button>
</a>
</div>
<?php } ?>
</div>
<!-- /.login-card-body -->
</div>
<!-- /.div.card -->
</div>
<!-- /.login-box -->
<?php
if (!$config_whitelabel_enabled) {
echo '<small class="text-muted">Powered by ITFlow</small>';
}
?>
<!-- jQuery -->
<script src="../plugins/jquery/jquery.min.js"></script>
<!-- Bootstrap 4 -->
<script src="../plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="../plugins/adminlte/js/adminlte.min.js"></script>
<!-- Prevents resubmit on refresh or back -->
<script src="../js/login_prevent_resubmit.js"></script>
</body>
</html>

View File

@@ -133,20 +133,20 @@ if (isset($_POST['code']) && $_POST['state'] == session_id()) {
header("Location: index.php");
} else {
$_SESSION['login_message'] = 'Something went wrong with logging you in: Your account is not configured for Entra SSO. Please ensure you are setup in ITFlow as a contact and have Entra SSO configured.';
header("Location: index.php");
}
}
header('Location: index.php');
} else {
echo "Error getting access_token";
}
}

View File

@@ -12,7 +12,7 @@ require_once '../includes/load_global_settings.php';
if (empty($config_smtp_host)) {
header("Location: login.php");
header("Location: /login.php");
exit();
}
@@ -72,7 +72,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") {
$name = sanitizeInput($row['contact_name']);
$client = intval($row['contact_client_id']);
$token = randomString(156);
$token = randomString(32);
$url = "https://$config_base_url/client/login_reset.php?email=$email&token=$token&client=$client";
mysqli_query($mysqli, "UPDATE users SET user_password_reset_token = '$token' WHERE user_id = $user_id LIMIT 1");
mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Contact', log_action = 'Modify', log_description = 'Sent a portal password reset e-mail for $email.', log_ip = '$ip', log_user_agent = '$user_agent', log_client_id = $client");
@@ -157,7 +157,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") {
// Redirect to login page
$_SESSION['login_message'] = "Password reset successfully!";
header("Location: login.php");
header("Location: /login.php");
exit();
} else {
@@ -275,7 +275,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") {
?>
</p>
<a href="login.php">Back to login</a>
<a href="/login.php">Back to login</a>
</div>

View File

@@ -25,7 +25,7 @@ if (isset($_POST['add_ticket'])) {
$config_ticket_new_ticket_notification_email = filter_var($config_ticket_new_ticket_notification_email, FILTER_VALIDATE_EMAIL);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
// Ensure priority is low/med/high (as can be user defined)
if ($_POST['priority'] !== "Low" && $_POST['priority'] !== "Medium" && $_POST['priority'] !== "High") {
@@ -185,6 +185,43 @@ if (isset($_POST['add_ticket_comment'])) {
}
}
if (isset($_GET['approve_ticket_task'])) {
$task_id = intval($_GET['approve_ticket_task']);
$approval_id = intval($_GET['approval_id']);
$url_key = sanitizeInput($_GET['approval_url_key']);
$approval_row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending' AND approval_scope = 'client'"));
$task_name = nullable_htmlentities($approval_row['task_name']);
$scope = nullable_htmlentities($approval_row['approval_scope']);
$type = nullable_htmlentities($approval_row['approval_type']);
$required_user = intval($approval_row['approval_required_user_id']);
$created_by = intval($approval_row['approval_created_by']);
$ticket_id = intval($approval_row['task_ticket_id']);
if (!$approval_row) {
flash_alert("Cannot find/approve that task", 'warning');
redirect();
exit;
}
// Approve
mysqli_query($mysqli, "UPDATE task_approvals SET approval_status = 'approved', approval_approved_by = $session_user_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending' AND approval_scope = 'client'");
// Notify tech
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$session_contact_email approved ticket task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_client_id = $session_client_id, notification_user_id = $created_by");
// TODO: Email agent
// Logging
logAction("Task", "Edit", "Contact $session_contact_email approved task $task_name (approval $approval_id)", $session_client_id, $task_id);
flash_alert("Task Approved");
redirect();
}
if (isset($_POST['add_ticket_feedback'])) {
$ticket_id = intval($_POST['ticket_id']);
@@ -320,7 +357,7 @@ if (isset($_GET['logout'])) {
session_unset();
session_destroy();
redirect('login.php');
redirect('/login.php');
}

View File

@@ -20,9 +20,9 @@ require_once 'includes/inc_all.php';
<p>Client Primary Contact: <?php if ($session_contact_primary == 1) {echo "Yes"; } else {echo "No";} ?></p>
<p>Client Technical Contact: <?php if ($session_contact_is_technical_contact) {echo "Yes"; } else {echo "No";} ?></p>
<p>Client Billing Contact: <?php if ($session_contact_is_billing_contact == $session_contact_id) {echo "Yes"; } else {echo "No";} ?></p>
<br>
<p>Login via: <?php echo $_SESSION['login_method'] ?> </p>
<p>User ID: <?php echo $_SESSION['user_id'] ?> </p>
<!-- // Show option to change password if auth provider is local -->

View File

@@ -70,6 +70,13 @@ if (isset($_GET['id']) && intval($_GET['id'])) {
);
$completed_task_count = mysqli_num_rows($sql_tasks_completed);
// Get pending task approvals
$sql_task_approvals = mysqli_query($mysqli,"
SELECT task_id, task_name, approval_id, approval_scope, approval_type, approval_required_user_id, approval_status, approval_url_key
FROM tasks
LEFT JOIN task_approvals ON task_id = task_approvals.approval_task_id
WHERE task_ticket_id = $ticket_id AND task_completed_at IS NULL AND approval_scope = 'client' AND approval_status = 'pending'
");
?>
<ol class="breadcrumb d-print-none">
@@ -130,6 +137,59 @@ if (isset($_GET['id']) && intval($_GET['id'])) {
</div>
</div>
<!-- Approvals -->
<?php if (mysqli_num_rows($sql_task_approvals) > 0) { ?>
<div class="card">
<div class="card-body">
<h5>Approvals</h5>
This ticket has tasks requiring approval:
<ul>
<?php
while ($approvals = mysqli_fetch_array($sql_task_approvals)) {
$task_id = intval($approvals['task_id']);
$approval_id = intval($approvals['approval_id']);
$task_name = nullable_htmlentities($approvals['task_name']);
$approval_type = nullable_htmlentities($approvals['approval_type']);
$approval_url_key = nullable_htmlentities($approvals['approval_url_key']);
$contact_can_approve = false; // Default
if ($approval_type == 'any') {
$contact_can_approve = true;
}
if ($session_contact_primary) {
$contact_can_approve = true;
}
if ($approval_type == 'technical' && $session_contact_is_technical_contact) {
$contact_can_approve = true;
}
if ($approval_type == 'billing' && $session_contact_is_billing_contact) {
$contact_can_approve = true;
}
?>
<li>
<?php echo $task_name;
if ($contact_can_approve) { ?> - <a href="post.php?approve_ticket_task=<?= $task_id ?>&approval_id=<?= $approval_id ?>&approval_url_key=<?= $approval_url_key ?>" class="confirm-link">Approve task</a> <?php }
else {?> - Please ask your <?= $approval_type ?> contact to approve this task <?php } ?>
</li>
<?php } ?>
</ul>
</div>
</div>
<?php } ?>
<hr>
<!-- Either show the reply comments box, option to re-open ticket, show ticket smiley feedback or thanks for feedback -->

View File

@@ -615,7 +615,7 @@ while ($row = mysqli_fetch_array($sql_recurring_invoices)) {
$new_invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli, "INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Sent', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id");

View File

@@ -106,7 +106,7 @@ function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date
$contact_email_esc = mysqli_real_escape_string($mysqli, $contact_email);
$client_id = intval($client_id);
$url_key = randomString(156);
$url_key = randomString(32);
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$ticket_prefix_esc', ticket_number = $ticket_number, ticket_source = 'Email', ticket_subject = '$subject', ticket_details = '$message_esc', ticket_priority = 'Low', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_url_key = '$url_key', ticket_client_id = $client_id");
$id = mysqli_insert_id($mysqli);

19
db.sql
View File

@@ -2441,6 +2441,25 @@ CREATE TABLE `tasks` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `task_approvals`
--
DROP TABLE IF EXISTS `task_approvals`;
CREATE TABLE IF NOT EXISTS `task_approvals` (
`approval_id` int(11) NOT NULL AUTO_INCREMENT,
`approval_scope` enum('client','internal') NOT NULL,
`approval_type` enum('any','technical','billing','specific') NOT NULL,
`approval_required_user_id` int(11) DEFAULT NULL,
`approval_status` enum('pending','approved','declined') NOT NULL,
`approval_created_by` int(11) NOT NULL,
`approval_approved_by` varchar(255) DEFAULT NULL,
`approval_url_key` varchar(200) NOT NULL,
`approval_task_id` int(11) NOT NULL,
PRIMARY KEY (`approval_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `taxes`
--

View File

@@ -4,20 +4,13 @@
DEFINE("WORDING_ROLECHECK_FAILED", "You are not permitted to do that!");
// Function to generate both crypto & URL safe random strings
function randomString($length = 16) {
// Generate some cryptographically safe random bytes
// Generate a little more than requested as we'll lose some later converting
$random_bytes = random_bytes($length + 5);
// Convert the bytes to something somewhat human-readable
$random_base_64 = base64_encode($random_bytes);
// Replace the nasty characters that come with base64
$bad_chars = array("/", "+", "=");
$random_string = str_replace($bad_chars, random_int(0, 9), $random_base_64);
// Truncate the string to the requested $length and return
return substr($random_string, 0, $length);
function randomString(int $length = 16): string {
$bytes = random_bytes((int) ceil($length * 3 / 4));
return substr(
rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='),
0,
$length
);
}
// Older keygen function - only used for TOTP currently
@@ -1443,6 +1436,10 @@ function appNotify($type, $details, $action = null, $client_id = 0, $entity_id =
function logAction($type, $action, $description, $client_id = 0, $entity_id = 0) {
global $mysqli, $session_user_agent, $session_ip, $session_user_id;
$client_id = intval($client_id);
$entity_id = intval($entity_id);
$session_user_id = intval($session_user_id);
if (empty($session_user_id)) {
$session_user_id = 0;
}
@@ -1784,3 +1781,220 @@ function cleanupUnusedImages(string $html, string $folderFsPath, string $folderW
}
}
}
/**
* Simple mysqli helper functions
* - Prepared statements under the hood
* - "Old style" INSERT/UPDATE SET feeling
*/
/**
* Core executor: prepares, binds, executes.
*
* @throws Exception on error
*/
function dbExecute(mysqli $mysqli, string $sql, array $params = []): mysqli_stmt
{
$stmt = $mysqli->prepare($sql);
if (!$stmt) {
throw new Exception('MySQLi prepare error: ' . $mysqli->error . ' | SQL: ' . $sql);
}
if (!empty($params)) {
$types = '';
$values = [];
foreach ($params as $param) {
if (is_int($param)) {
$types .= 'i';
} elseif (is_float($param)) {
$types .= 'd';
} elseif (is_bool($param)) {
$types .= 'i';
$param = $param ? 1 : 0;
} elseif (is_null($param)) {
$types .= 's';
$param = null;
} else {
$types .= 's';
}
$values[] = $param;
}
if (!$stmt->bind_param($types, ...$values)) {
throw new Exception('MySQLi bind_param error: ' . $stmt->error . ' | SQL: ' . $sql);
}
}
if (!$stmt->execute()) {
throw new Exception('MySQLi execute error: ' . $stmt->error . ' | SQL: ' . $sql);
}
return $stmt;
}
/**
* Fetch all rows as associative arrays.
*/
function dbFetchAll(mysqli $mysqli, string $sql, array $params = []): array
{
$stmt = dbExecute($mysqli, $sql, $params);
$result = $stmt->get_result();
if ($result === false) {
return [];
}
return $result->fetch_all(MYSQLI_ASSOC);
}
/**
* Fetch a single row (assoc) or null if none.
*/
function dbFetchOne(mysqli $mysqli, string $sql, array $params = []): ?array
{
$stmt = dbExecute($mysqli, $sql, $params);
$result = $stmt->get_result();
if ($result === false) {
return null;
}
$row = $result->fetch_assoc();
return $row !== null ? $row : null;
}
/**
* Fetch a single scalar value (first column of first row) or null.
*/
function dbFetchValue(mysqli $mysqli, string $sql, array $params = [])
{
$row = dbFetchOne($mysqli, $sql, $params);
if ($row === null) {
return null;
}
return reset($row);
}
/**
* INSERT using "SET" style.
* Example:
* $id = dbInsert($mysqli, 'clients', [
* 'client_name' => $name,
* 'client_type' => $type,
* ]);
*
* @return int insert_id
*
* @throws InvalidArgumentException
* @throws Exception
*/
function dbInsert(mysqli $mysqli, string $table, array $data): int
{
if (empty($data)) {
throw new InvalidArgumentException('dbInsert called with empty $data');
}
$setParts = [];
foreach ($data as $column => $_) {
$setParts[] = "$column = ?";
}
$sql = "INSERT INTO $table SET " . implode(', ', $setParts);
$params = array_values($data);
dbExecute($mysqli, $sql, $params);
return $mysqli->insert_id;
}
function dbUpdate(
mysqli $mysqli,
string $table,
array $data,
$where,
array $whereParams = []
): int {
if (empty($data)) {
throw new InvalidArgumentException('dbUpdate called with empty $data');
}
if (empty($where)) {
throw new InvalidArgumentException('dbUpdate requires a WHERE clause');
}
$setParts = [];
foreach ($data as $column => $_) {
$setParts[] = "$column = ?";
}
if (is_array($where)) {
$whereParts = [];
$whereParams = [];
foreach ($where as $column => $value) {
$whereParts[] = "$column = ?";
$whereParams[] = $value;
}
$whereSql = implode(' AND ', $whereParts);
} else {
$whereSql = $where;
}
$sql = "UPDATE $table SET " . implode(', ', $setParts) . " WHERE $whereSql";
$params = array_merge(array_values($data), $whereParams);
$stmt = dbExecute($mysqli, $sql, $params);
return $stmt->affected_rows;
}
/**
* DELETE helper.
*
* WHERE can be:
* - array: ['client_id' => $id] (auto "client_id = ?")
* - string: 'client_id = ?' (use with $whereParams)
*
* @return int affected_rows
*
* @throws InvalidArgumentException
* @throws Exception
*/
function dbDelete(
mysqli $mysqli,
string $table,
$where,
array $whereParams = []
): int {
if (empty($where)) {
throw new InvalidArgumentException('dbDelete requires a WHERE clause');
}
if (is_array($where)) {
$whereParts = [];
$whereParams = [];
foreach ($where as $column => $value) {
$whereParts[] = "$column = ?";
$whereParams[] = $value;
}
$whereSql = implode(' AND ', $whereParts);
} else {
$whereSql = $where;
}
$sql = "DELETE FROM $table WHERE $whereSql";
$stmt = dbExecute($mysqli, $sql, $whereParams);
return $stmt->affected_rows;
}
/**
* Transaction helpers (optional sugar).
*/
function dbBegin(mysqli $mysqli): void
{
$mysqli->begin_transaction();
}
function dbCommit(mysqli $mysqli): void
{
$mysqli->commit();
}
function dbRollback(mysqli $mysqli): void
{
$mysqli->rollback();
}

View File

@@ -0,0 +1,114 @@
<?php
require_once "includes/inc_all_guest.php";
//Initialize the HTML Purifier to prevent XSS
require_once "../plugins/htmlpurifier/HTMLPurifier.standalone.php";
$purifier_config = HTMLPurifier_Config::createDefault();
$purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
$purifier = new HTMLPurifier($purifier_config);
if (!isset($_GET['task_approval_id'], $_GET['url_key'])) {
echo "<br><h2>Oops, something went wrong! Please raise a ticket if you believe this is an error.</h2>";
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
exit();
}
// Company info
$company_sql_row = mysqli_fetch_array(mysqli_query($mysqli, "
SELECT
company_phone,
company_phone_country_code,
company_website
FROM
companies,
settings
WHERE
companies.company_id = settings.company_id
AND companies.company_id = 1"
));
$company_phone_country_code = nullable_htmlentities($company_sql_row['company_phone_country_code']);
$company_phone = nullable_htmlentities(formatPhoneNumber($company_sql_row['company_phone'], $company_phone_country_code));
$company_website = nullable_htmlentities($company_sql_row['company_website']);
$approval_id = intval($_GET['task_approval_id']);
$url_key = sanitizeInput($_GET['url_key']);
$task_row = mysqli_fetch_assoc(mysqli_query($mysqli,
"SELECT * FROM task_approvals
LEFT JOIN tasks ON approval_task_id = task_id
LEFT JOIN tickets on task_ticket_id = ticket_id
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
WHERE approval_id = $approval_id AND approval_url_key = '$url_key'
LIMIT 1"
));
if (!$task_row) {
// Invalid
echo "<br><h2>Oops, something went wrong! Please raise a ticket if you believe this is an error.</h2>";
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
exit();
}
$task_id = intval($task_row['task_id']);
$task_name = nullable_htmlentities($task_row['task_name']);
$approval_scope = nullable_htmlentities($task_row['approval_scope']);
$approval_type = nullable_htmlentities($task_row['approval_type']);
$approval_status = nullable_htmlentities($task_row['approval_status']);
$ticket_prefix = nullable_htmlentities($task_row['ticket_prefix']);
$ticket_number = intval($task_row['ticket_number']);
$ticket_status = nullable_htmlentities($task_row['ticket_status_name']);
$ticket_priority = nullable_htmlentities($task_row['ticket_priority']);
$ticket_subject = nullable_htmlentities($task_row['ticket_subject']);
$ticket_details = $purifier->purify($task_row['ticket_details']);
?>
<div class="card mt-3">
<div class="card-header bg-dark text-center">
<h4 class="mt-1">
Task Approval for Ticket <?php echo $ticket_prefix, $ticket_number ?>
</h4>
</div>
<div class="card-body prettyContent">
<h5><strong>Subject:</strong> <?php echo $ticket_subject ?></h5>
<p>
<strong>State:</strong> <?php echo $ticket_status ?>
<br>
<strong>Priority:</strong> <?php echo $ticket_priority ?>
<br>
</p>
<?php echo $ticket_details ?>
<hr>
<h5>Task Approval</h5>
<p>
<strong>Task Name: </strong><?= ucfirst($task_name); ?>
<br>
<strong>Scope/Type:</strong> <?= ucfirst($approval_scope) . " - " . ucfirst($approval_type)?>
<br>
<strong>Status:</strong> <?= ucfirst($approval_status)?>
<br>
<?php
if ($approval_status == 'pending') { ?>
<strong>Action: </strong><a href="guest_post.php?approve_ticket_task=<?= $task_id ?>&approval_id=<?= $approval_id ?>&approval_url_key=<?= $url_key ?>" class="confirm-link text-bold">Approve Task</a>
<?php } ?>
</p>
</div>
</div>
<hr>
<div class="card-footer">
<?php echo "<i class='fas fa-phone fa-fw mr-2'></i>$company_phone | <i class='fas fa-globe fa-fw mr-2 ml-2'></i>$company_website"; ?>
</div>
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';

View File

@@ -225,6 +225,39 @@ if (isset($_GET['add_ticket_feedback'], $_GET['url_key'])) {
}
if (isset($_GET['approve_ticket_task'])) {
$task_id = intval($_GET['approve_ticket_task']);
$approval_id = intval($_GET['approval_id']);
$url_key = sanitizeInput($_GET['approval_url_key']);
$approval_row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending'"));
$task_name = nullable_htmlentities($approval_row['task_name']);
$scope = nullable_htmlentities($approval_row['approval_scope']);
$type = nullable_htmlentities($approval_row['approval_type']);
$required_user = intval($approval_row['approval_required_user_id']);
$created_by = intval($approval_row['approval_created_by']);
$ticket_id = intval($approval_row['task_ticket_id']);
if (!$approval_row) {
exit("Cannot find/approve that task");
}
// Approve
mysqli_query($mysqli, "UPDATE task_approvals SET approval_status = 'approved', approval_approved_by = $required_user WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending'");
// Notify tech
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = 'Guest approved ticket task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_user_id = $created_by");
// Logging
logAction("Task", "Edit", "Guest user approved task $task_name via link (approval $approval_id)", 0, $task_id);
flash_alert("Task Approved");
redirect();
}
if (isset($_GET['export_quote_pdf'])) {
$quote_id = intval($_GET['export_quote_pdf']);

View File

@@ -5,4 +5,4 @@
* Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month.
*/
DEFINE("APP_VERSION", "25.12");
DEFINE("APP_VERSION", "25.12.1");

View File

@@ -5,4 +5,4 @@
* It is used in conjunction with database_updates.php
*/
DEFINE("LATEST_DATABASE_VERSION", "2.3.8");
DEFINE("LATEST_DATABASE_VERSION", "2.3.9");

882
login.php
View File

@@ -1,329 +1,586 @@
<?php
// Enforce a Content Security Policy for security against cross-site scripting
// Unified login (Agent + Client) using one email & password
header("Content-Security-Policy: default-src 'self'");
// Check if the config.php file exists
if (!file_exists('config.php')) {
// Redirect to the setup page if config.php doesn't exist
header("Location: /setup"); // Must use header as functions aren't included yet
exit();
}
require_once "config.php";
// Check if setup mode is enabled or the variable is missing
if (!isset($config_enable_setup) || $config_enable_setup == 1) {
// Redirect to the setup page
header("Location: /setup");
exit();
}
// Set Timezone
require_once "includes/inc_set_timezone.php";
require_once "config.php";
require_once "functions.php";
require_once "plugins/totp/totp.php";
// Check if the application is configured for HTTPS-only access
if ($config_https_only && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') && (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')) {
if (session_status() === PHP_SESSION_NONE) {
ini_set("session.cookie_httponly", true);
if ($config_https_only || !isset($config_https_only)) {
ini_set("session.cookie_secure", true);
}
session_start();
}
if (!isset($config_enable_setup) || $config_enable_setup == 1) {
header("Location: /setup");
exit();
}
if (
$config_https_only
&& (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on')
&& (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')
) {
echo "Login is restricted as ITFlow defaults to HTTPS-only for enhanced security. To login using HTTP, modify the config.php file by setting config_https_only to false. However, this is strongly discouraged, especially when accessing from potentially unsafe networks like the internet.";
exit;
}
require_once "functions.php";
require_once "includes/inc_set_timezone.php";
require_once "plugins/totp/totp.php";
// IP & User Agent for logging
$session_ip = sanitizeInput(getIP());
$session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT']);
// Block brute force password attacks - check recent failed login attempts for this IP
// Block access if more than 15 failed login attempts have happened in the last 10 minutes
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(log_id) AS failed_login_count FROM logs WHERE log_ip = '$session_ip' AND log_type = 'Login' AND log_action = 'Failed' AND log_created_at > (NOW() - INTERVAL 10 MINUTE)"));
$session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT'] ?? '');
$row = mysqli_fetch_assoc(mysqli_query(
$mysqli,
"SELECT COUNT(log_id) AS failed_login_count
FROM logs
WHERE log_ip = '$session_ip'
AND log_type = 'Login'
AND log_action = 'Failed'
AND log_created_at > (NOW() - INTERVAL 10 MINUTE)"
));
$failed_login_count = intval($row['failed_login_count']);
if ($failed_login_count >= 15) {
logAction("Login", "Blocked", "$session_ip was blocked access to login due to IP lockout");
// Inform user & quit processing page
header("HTTP/1.1 429 Too Many Requests");
exit("<h2>$config_app_name</h2>Your IP address has been blocked due to repeated failed login attempts. Please try again later. <br><br>This action has been logged.");
}
// Query Settings for company
$sql_settings = mysqli_query($mysqli, "SELECT * FROM settings LEFT JOIN companies ON settings.company_id = companies.company_id WHERE settings.company_id = 1");
// Settings
$sql_settings = mysqli_query($mysqli, "
SELECT settings.*, companies.company_name, companies.company_logo
FROM settings
LEFT JOIN companies ON settings.company_id = companies.company_id
WHERE settings.company_id = 1
");
$row = mysqli_fetch_array($sql_settings);
// Company info
$company_name = $row['company_name'];
$company_logo = $row['company_logo'];
$config_start_page = nullable_htmlentities($row['config_start_page']);
$config_login_message = nullable_htmlentities($row['config_login_message']);
$company_name = $row['company_name'];
$company_logo = $row['company_logo'];
$config_start_page = nullable_htmlentities($row['config_start_page']);
$config_login_message = nullable_htmlentities($row['config_login_message']);
// Mail
$config_smtp_host = $row['config_smtp_host'];
$config_smtp_port = intval($row['config_smtp_port']);
$config_smtp_host = $row['config_smtp_host'];
$config_smtp_port = intval($row['config_smtp_port']);
$config_smtp_encryption = $row['config_smtp_encryption'];
$config_smtp_username = $row['config_smtp_username'];
$config_smtp_password = $row['config_smtp_password'];
$config_smtp_username = $row['config_smtp_username'];
$config_smtp_password = $row['config_smtp_password'];
$config_mail_from_email = sanitizeInput($row['config_mail_from_email']);
$config_mail_from_name = sanitizeInput($row['config_mail_from_name']);
// Client Portal Enabled
$config_client_portal_enable = intval($row['config_client_portal_enable']);
// Login key (if setup)
$config_login_key_required = $row['config_login_key_required'];
$config_login_key_secret = $row['config_login_key_secret'];
$config_mail_from_name = sanitizeInput($row['config_mail_from_name']);
$config_client_portal_enable = intval($row['config_client_portal_enable']);
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
// Login key verification
// If no/incorrect 'key' is supplied, send to client portal instead
if ($config_login_key_required) {
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
redirect("client");
}
$config_login_key_required = $row['config_login_key_required'];
$config_login_key_secret = $row['config_login_key_secret'];
$azure_client_id = $row['config_azure_client_id'] ?? null;
$response = null;
$token_field = null;
$show_role_choice = false;
$email = '';
$password = ''; // only ever used in the initial POST request
// Helpers
function pendingExpired($sess, $ttl_seconds = 120) {
return !$sess || empty($sess['created']) || (time() - intval($sess['created']) > $ttl_seconds);
}
// HTTP-Only cookies
ini_set("session.cookie_httponly", true);
// POST handling
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']) || isset($_POST['mfa_login']))) {
// Tell client to only send cookie(s) over HTTPS
if ($config_https_only || !isset($config_https_only)) {
ini_set("session.cookie_secure", true);
}
$role_choice = $_POST['role_choice'] ?? null;
// Handle POST login request
if (isset($_POST['login'])) {
$is_login_step = isset($_POST['login']);
$is_role_step = isset($_POST['role_choice']) && !$is_login_step && !isset($_POST['mfa_login']);
$is_mfa_step = isset($_POST['mfa_login']);
// Sessions should start after the user has POSTed data
session_start();
// -----------------------------------
// STEP 2: ROLE CHOICE (no email/pass)
// -----------------------------------
if ($is_role_step) {
// Passed login brute force check
$email = sanitizeInput($_POST['email']);
$password = $_POST['password'];
$posted_token = $_POST['pending_login_token'] ?? '';
$sess = $_SESSION['pending_dual_login'] ?? null;
$current_code = 0; // Default value
if (isset($_POST['current_code'])) {
$current_code = intval($_POST['current_code']);
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM users LEFT JOIN user_settings on users.user_id = user_settings.user_id WHERE user_email = '$email' AND user_archived_at IS NULL AND user_status = 1 AND user_type = 1"));
// Check password
if ($row && password_verify($password, $row['user_password'])) {
// User password correct (partial login)
// Set temporary user variables
$user_name = sanitizeInput($row['user_name']);
$user_id = intval($row['user_id']);
$session_user_id = $user_id; // to pass the user_id to logAction function
$user_email = sanitizeInput($row['user_email']);
$token = sanitizeInput($row['user_token']);
$force_mfa = intval($row['user_config_force_mfa']);
$user_role_id = intval($row['user_role_id']);
$user_encryption_ciphertext = $row['user_specific_encryption_ciphertext'];
$user_extension_key = $row['user_extension_key'];
$mfa_is_complete = false; // Default to requiring MFA
$extended_log = ''; // Default value
if (empty($token)) {
// MFA is not configured
$mfa_is_complete = true;
}
// Validate MFA via a remember-me cookie
if (isset($_COOKIE['rememberme'])) {
// Get remember tokens less than $config_login_remember_me_days_expire days old
$remember_tokens = mysqli_query($mysqli, "SELECT remember_token_token FROM remember_tokens WHERE remember_token_user_id = $user_id AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY)");
while ($row = mysqli_fetch_assoc($remember_tokens)) {
if (hash_equals($row['remember_token_token'], $_COOKIE['rememberme'])) {
$mfa_is_complete = true;
$extended_log = 'with 2FA remember-me cookie';
break;
}
}
}
// Validate MFA code
if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) {
$mfa_is_complete = true;
$extended_log = 'with MFA';
}
if ($mfa_is_complete) {
// MFA Completed successfully
// FULL LOGIN SUCCESS
// Create a remember me token, if requested
if (isset($_POST['remember_me'])) {
// TODO: Record the UA and IP a token is generated from so that can be shown later on
$newRememberToken = bin2hex(random_bytes(64));
setcookie('rememberme', $newRememberToken, time() + 86400*$config_login_remember_me_expire, "/", null, true, true);
mysqli_query($mysqli, "INSERT INTO remember_tokens SET remember_token_user_id = $user_id, remember_token_token = '$newRememberToken'");
$extended_log .= ", generated a new remember-me token";
}
// Check this login isn't suspicious
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(log_id) AS ip_previous_logins FROM logs WHERE log_type = 'Login' AND log_action = 'Success' AND log_ip = '$session_ip' AND log_user_id = $user_id"));
$ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']);
$sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(log_id) AS ua_previous_logins FROM logs WHERE log_type = 'Login' AND log_action = 'Success' AND log_user_agent = '$session_user_agent' AND log_user_id = $user_id"));
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
// Notify if both the user agent and IP are different
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
$subject = "$config_app_name new login for $user_name";
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [
[
'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name,
'recipient' => $user_email,
'recipient_name' => $user_name,
'subject' => $subject,
'body' => $body
]
];
addToMailQueue($data);
}
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
// Session info
$_SESSION['user_id'] = $user_id;
$_SESSION['csrf_token'] = randomString(156);
$_SESSION['logged'] = true;
// Forcing MFA
if ($force_mfa == 1 && $token == NULL) {
$config_start_page = "user/mfa_enforcement.php";
}
// Setup encryption session key
if (isset($user_encryption_ciphertext)) {
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
generateUserSessionKey($site_encryption_master_key);
// Setup extension - currently unused
//if (is_null($user_extension_key)) {
// Extension cookie
// Note: Browsers don't accept cookies with SameSite None if they are not HTTPS.
//setcookie("user_extension_key", "$user_extension_key", ['path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'None']);
// Set PHP session in DB, so we can access the session encryption data (above)
//$user_php_session = session_id();
//mysqli_query($mysqli, "UPDATE users SET user_php_session = '$user_php_session' WHERE user_id = $user_id");
//}
}
// Redirect to last visited or config home
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']) );
} else {
redirect("agent/$config_start_page");
}
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) {
unset($_SESSION['pending_dual_login']);
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Your login session expired. Please sign in again.
</div>";
} else {
// MFA is configured and needs to be confirmed, or was unsuccessful
// HTML code for the token input field
$token_field = "
<div class='input-group mb-3'>
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6' class='form-control' placeholder='Enter your 2FA code' name='current_code' required autofocus>
<div class='input-group-append'>
<div class='input-group-text'>
<span class='fas fa-key'></span>
</div>
</div>
</div>";
// Log/notify if MFA was unsuccessful
if ($current_code !== 0) {
// Logging
logAction("Login", "MFA Failed", "$user_name failed MFA", 0, $user_id);
// Email the tech to advise their credentials may be compromised
if (!empty($config_smtp_host)) {
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [
[
'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name,
'recipient' => $user_email,
'recipient_name' => $user_name,
'subject' => $subject,
'body' => $body
]
];
$mail = addToMailQueue($data);
}
// HTML feedback for incorrect 2FA code
$response = "
<div class='alert alert-warning'>
Please Enter 2FA Code!
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
}
$email = sanitizeInput($sess['email'] ?? '');
}
}
} else {
// -----------------------------------
// STEP 3: MFA SUBMIT (no email/pass)
// -----------------------------------
if ($is_mfa_step && empty($response)) {
// Password incorrect or user doesn't exist - show generic error
$posted_token = $_POST['pending_mfa_token'] ?? '';
$sess = $_SESSION['pending_mfa_login'] ?? null;
header("HTTP/1.1 401 Unauthorized");
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) {
unset($_SESSION['pending_mfa_login']);
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Your MFA session expired. Please sign in again.
</div>";
} else {
$email = sanitizeInput($sess['email'] ?? '');
$role_choice = 'agent';
}
}
logAction("Login", "Failed", "Failed login attempt using $email");
// -----------------------------------
// STEP 1: INITIAL CREDENTIALS
// -----------------------------------
if ($is_login_step && empty($response)) {
$email = sanitizeInput($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$response = "
if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
}
}
// Continue only if no response error
if (empty($response)) {
// Query all possible matches for that email
$sql = mysqli_query($mysqli, "
SELECT users.*,
user_settings.*,
contacts.*,
clients.*
FROM users
LEFT JOIN user_settings ON users.user_id = user_settings.user_id
LEFT JOIN contacts ON users.user_id = contacts.contact_user_id
LEFT JOIN clients ON contacts.contact_client_id = clients.client_id
WHERE user_email = '$email'
AND user_archived_at IS NULL
AND user_status = 1
AND (
user_type = 1
OR (user_type = 2 AND client_archived_at IS NULL)
)
");
$agentRow = null;
$clientRow = null;
// Step 1: verify password. Step 2/3: use stored allowed ids.
$allowed_agent_id = null;
$allowed_client_id = null;
if ($is_role_step) {
$sess = $_SESSION['pending_dual_login'] ?? null;
$allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null;
$allowed_client_id = isset($sess['client_user_id']) ? intval($sess['client_user_id']) : null;
}
if ($is_mfa_step) {
$sess = $_SESSION['pending_mfa_login'] ?? null;
$allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null;
}
while ($r = mysqli_fetch_assoc($sql)) {
$ut = intval($r['user_type']);
if ($is_login_step) {
// Only Step 1 checks password
if (!password_verify($password, $r['user_password'])) {
continue;
}
} else {
// Step 2/3: restrict to ids we previously verified
if ($ut === 1 && $allowed_agent_id !== null && intval($r['user_id']) !== $allowed_agent_id) {
continue;
}
if ($ut === 2 && $allowed_client_id !== null && intval($r['user_id']) !== $allowed_client_id) {
continue;
}
}
if ($ut === 1 && $agentRow === null) {
$agentRow = $r;
}
if ($ut === 2 && $clientRow === null) {
$clientRow = $r;
}
}
if ($agentRow === null && $clientRow === null) {
header("HTTP/1.1 401 Unauthorized");
logAction("Login", "Failed", "Failed login attempt using $email");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
</div>";
} else {
$selectedRow = null;
$selectedType = null; // 1 agent, 2 client
// Dual role
if ($agentRow !== null && $clientRow !== null) {
if ($role_choice === 'agent') {
$selectedRow = $agentRow;
$selectedType = 1;
} elseif ($role_choice === 'client') {
$selectedRow = $clientRow;
$selectedType = 2;
} else {
// Show role choice screen
$show_role_choice = true;
// If this is the first time (Step 1), we need to stash allowed ids and (optional) decrypted agent encryption key
// WITHOUT storing password.
if ($is_login_step) {
$pending_token = bin2hex(random_bytes(32));
// If agent has user-specific encryption ciphertext, decrypt it NOW while password is present.
$agent_master_key = null;
$agent_cipher = $agentRow['user_specific_encryption_ciphertext'] ?? null;
if (!empty($agent_cipher)) {
$agent_master_key = decryptUserSpecificKey($agent_cipher, $password);
}
$_SESSION['pending_dual_login'] = [
'email' => $email,
'agent_user_id' => intval($agentRow['user_id']),
'client_user_id' => intval($clientRow['user_id']),
'agent_master_key' => $agent_master_key, // may be null
'token' => $pending_token,
'created' => time()
];
}
}
} else {
// Single role
if ($agentRow !== null) {
$selectedRow = $agentRow;
$selectedType = 1;
} else {
$selectedRow = $clientRow;
$selectedType = 2;
}
}
// Proceed if selected
if ($selectedRow !== null && $selectedType !== null) {
// Clear dual pending once we actually proceed
unset($_SESSION['pending_dual_login']);
$user_id = intval($selectedRow['user_id']);
$user_email = sanitizeInput($selectedRow['user_email']);
// =========================
// AGENT FLOW
// =========================
if ($selectedType === 1) {
if ($config_login_key_required) {
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
redirect();
}
}
$user_name = sanitizeInput($selectedRow['user_name']);
$token = sanitizeInput($selectedRow['user_token']);
$force_mfa = intval($selectedRow['user_config_force_mfa']);
$user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext'];
$current_code = 0;
if (isset($_POST['current_code'])) {
$current_code = intval($_POST['current_code']);
}
$mfa_is_complete = false;
$extended_log = '';
if (empty($token)) {
$mfa_is_complete = true; // no MFA configured
}
// remember-me cookie allows bypass
if (isset($_COOKIE['rememberme'])) {
$remember_tokens = mysqli_query($mysqli, "
SELECT remember_token_token
FROM remember_tokens
WHERE remember_token_user_id = $user_id
AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY)
");
while ($remember_row = mysqli_fetch_assoc($remember_tokens)) {
if (hash_equals($remember_row['remember_token_token'], $_COOKIE['rememberme'])) {
$mfa_is_complete = true;
$extended_log = 'with 2FA remember-me cookie';
break;
}
}
}
// Validate MFA code
if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) {
$mfa_is_complete = true;
$extended_log = 'with MFA';
}
if ($mfa_is_complete) {
// Clear pending MFA if exists
unset($_SESSION['pending_mfa_login']);
// Remember me token creation
if (isset($_POST['remember_me'])) {
$newRememberToken = bin2hex(random_bytes(64));
setcookie('rememberme', $newRememberToken, time() + 86400 * $config_login_remember_me_expire, "/", null, true, true);
mysqli_query($mysqli, "
INSERT INTO remember_tokens
SET remember_token_user_id = $user_id,
remember_token_token = '$newRememberToken'
");
$extended_log .= ", generated a new remember-me token";
}
// Suspicious login checks / email notify (kept from your code)
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT COUNT(log_id) AS ip_previous_logins
FROM logs
WHERE log_type = 'Login'
AND log_action = 'Success'
AND log_ip = '$session_ip'
AND log_user_id = $user_id
"));
$ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']);
$sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT COUNT(log_id) AS ua_previous_logins
FROM logs
WHERE log_type = 'Login'
AND log_action = 'Success'
AND log_user_agent = '$session_user_agent'
AND log_user_id = $user_id
"));
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
$subject = "$config_app_name new login for $user_name";
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [[
'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name,
'recipient' => $user_email,
'recipient_name' => $user_name,
'subject' => $subject,
'body' => $body
]];
addToMailQueue($data);
}
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
$_SESSION['user_id'] = $user_id;
$_SESSION['csrf_token'] = randomString(32);
$_SESSION['logged'] = true;
if ($force_mfa == 1 && $token == NULL) {
$config_start_page = "user/mfa_enforcement.php";
}
// ✅ Setup encryption session key WITHOUT PASSWORD IN SESSION
// If we are coming from MFA step, master key is in pending_mfa_login.
// If we are coming from login step with no MFA, decrypt now.
$site_encryption_master_key = null;
if ($is_mfa_step) {
$sess = $_SESSION['pending_mfa_login'] ?? null;
if ($sess && isset($sess['agent_master_key'])) {
$site_encryption_master_key = $sess['agent_master_key'];
}
} else {
// No MFA step: password exists in this request (Step 1)
if (!empty($user_encryption_ciphertext)) {
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
}
}
if (!empty($site_encryption_master_key)) {
generateUserSessionKey($site_encryption_master_key);
}
// Redirect
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']));
} else {
redirect("agent/$config_start_page");
}
} else {
// MFA required — store *only what we need*, not password
$pending_mfa_token = bin2hex(random_bytes(32));
// If we arrived here from role-choice step, the agent master key may be in pending_dual_login
// If we arrived from initial login step, decrypt now (password in memory) and store master key.
$agent_master_key = null;
if ($is_role_step) {
$sess = $_SESSION['pending_dual_login'] ?? null;
if ($sess && isset($sess['agent_master_key'])) {
$agent_master_key = $sess['agent_master_key'];
}
} else {
if (!empty($user_encryption_ciphertext)) {
$agent_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
}
}
$_SESSION['pending_mfa_login'] = [
'email' => $user_email,
'agent_user_id' => $user_id,
'agent_master_key'=> $agent_master_key, // may be null
'token' => $pending_mfa_token,
'created' => time()
];
$token_field = "
<div class='input-group mb-3'>
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6'
class='form-control' placeholder='Verify your 2FA code'
name='current_code' required autofocus>
<div class='input-group-append'>
<div class='input-group-text'>
<span class='fas fa-key'></span>
</div>
</div>
</div>";
if ($current_code !== 0) {
logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id);
if (!empty($config_smtp_host)) {
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [[
'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name,
'recipient' => $user_email,
'recipient_name' => $user_name,
'subject' => $subject,
'body' => $body
]];
addToMailQueue($data);
}
$response = "
<div class='alert alert-danger'>
Please enter a valid 2FA code.
</div>";
}
}
// =========================
// CLIENT FLOW
// =========================
} elseif ($selectedType === 2) {
if ($config_client_portal_enable != 1) {
header("HTTP/1.1 401 Unauthorized");
logAction("Client Login", "Failed", "Client portal disabled; login attempt using $email");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
</div>";
} else {
$user_id = intval($selectedRow['contact_user_id']);
$client_id = intval($selectedRow['contact_client_id']);
$contact_id = intval($selectedRow['contact_id']);
$user_auth_method = sanitizeInput($selectedRow['user_auth_method']);
if ($client_id && $contact_id && $user_auth_method === 'local') {
$_SESSION['client_logged_in'] = true;
$_SESSION['client_id'] = $client_id;
$_SESSION['user_id'] = $user_id;
$_SESSION['user_type'] = 2;
$_SESSION['contact_id'] = $contact_id;
$_SESSION['login_method'] = "local";
logAction("Client Login", "Success", "Client contact $user_email successfully logged in locally", $client_id, $user_id);
header("Location: client/index.php");
exit();
} else {
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id);
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
</div>";
}
}
}
}
}
}
}
?>
// Form state
$show_mfa_form = (isset($token_field) && !empty($token_field));
$show_login_form = (!$show_role_choice && !$show_mfa_form);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title><?php echo nullable_htmlentities($company_name); ?> | Login</title>
<!-- Tell the browser to be responsive to screen width -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<!-- Font Awesome -->
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
<!--
Favicon
If Fav Icon exists else use the default one
-->
<?php if(file_exists('uploads/favicon.ico')) { ?>
<link rel="icon" type="image/x-icon" href="/uploads/favicon.ico">
<?php } ?>
<!-- Theme style -->
<link rel="stylesheet" href="plugins/adminlte/css/adminlte.min.css">
</head>
<body class="hold-transition login-page">
@@ -336,83 +593,108 @@ if (isset($_POST['login'])) {
<?php } ?>
</div>
<!-- /.login-logo -->
<div class="card">
<div class="card-body login-card-body">
<?php if (!empty($config_login_message)){ ?>
<p class="login-box-msg px-0"><?php echo nl2br($config_login_message); ?></p>
<p class="login-box-msg px-0"><?php echo nl2br($config_login_message); ?></p>
<?php } ?>
<?php if (isset($response)) { ?>
<p><?php echo $response; ?></p>
<p><?php echo $response; ?></p>
<?php } ?>
<form method="post">
<div class="input-group mb-3" <?php if (isset($token_field)) { echo "hidden"; } ?>>
<input type="text" class="form-control" placeholder="Agent Email" name="email" value="<?php if (isset($token_field)) { echo $email; }?>" required <?php if (!isset($token_field)) { echo "autofocus"; } ?> >
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
<?php if ($show_login_form): ?>
<!-- STEP 1: Email + Password -->
<div class="input-group mb-3">
<input type="text" class="form-control"
placeholder="<?php if ($config_login_key_required) { if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { echo "Client "; } } echo "Email"; ?>"
name="email"
value="<?php echo htmlspecialchars($email ?? '', ENT_QUOTES); ?>"
required autofocus
>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
</div>
</div>
</div>
</div>
<div class="input-group mb-3" <?php if (isset($token_field)) { echo "hidden"; } ?>>
<input type="password" class="form-control" placeholder="Agent Password" name="password" value="<?php if (isset($token_field)) { echo $password; } ?>" required>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
<div class="input-group mb-3">
<input type="password" class="form-control" placeholder="Password" name="password" required>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
</div>
<?php
if (isset($token_field)) {
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
<?php endif; ?>
echo $token_field;
?>
<?php if ($show_role_choice): ?>
<!-- STEP 2: Role choice only -->
<input type="hidden" name="pending_login_token"
value="<?php echo htmlspecialchars($_SESSION['pending_dual_login']['token'] ?? '', ENT_QUOTES); ?>">
<div class="form-group mb-3">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
<label class="custom-control-label" for="remember_me">Remember Me</label>
<div class="mb-2 text-center">
<button type="submit" class="btn btn-dark btn-block mb-2" name="role_choice" value="agent">
Log in as Agent
</button>
<button type="submit" class="btn btn-light btn-block" name="role_choice" value="client">
Log in as Client
</button>
</div>
</div>
<?php endif; ?>
<?php
<?php if ($show_mfa_form): ?>
<!-- STEP 3: MFA only -->
<?php echo $token_field; ?>
}
<input type="hidden" name="pending_mfa_token"
value="<?php echo htmlspecialchars($_SESSION['pending_mfa_login']['token'] ?? '', ENT_QUOTES); ?>">
?>
<div class="form-group mb-3">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
<label class="custom-control-label" for="remember_me">Remember Me</label>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
<?php if($config_client_portal_enable == 1){ ?>
<hr>
<h5 class="text-center">Looking for the <a href="client">Client Portal?<a/></h5>
<?php } ?>
<button type="submit" class="btn btn-dark btn-block mb-3" name="mfa_login">Verify & Sign In</button>
<?php endif; ?>
</form>
<?php if($config_client_portal_enable == 1){ ?>
<hr>
<?php if (!empty($config_smtp_host)) { ?>
<a href="client/login_reset.php">Forgot password?</a>
<?php } ?>
<?php if (!empty($azure_client_id)) { ?>
<div class="col text-center mt-2">
<a href="client/login_microsoft.php">
<button type="button" class="btn btn-secondary">Login with Microsoft Entra</button>
</a>
</div>
<?php } ?>
<?php } ?>
</div>
<!-- /.login-card-body -->
</div>
</div>
<!-- /.login-box -->
<!-- jQuery -->
<?php
if (!$config_whitelabel_enabled) {
echo '<small class="text-muted">Powered by ITFlow</small>';
}
?>
<script src="plugins/jquery/jquery.min.js"></script>
<!-- Bootstrap 4 -->
<script src="plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="plugins/adminlte/js/adminlte.min.js"></script>
<!-- <script src="plugins/Show-Hide-Passwords-Bootstrap-4/bootstrap-show-password.min.js"></script> -->
<!-- Prevents resubmit on refresh or back -->
<script src="js/login_prevent_resubmit.js"></script>
</body>