3 Commits

Author SHA1 Message Date
Johnny
03570ecd04 Merge pull request #1250 from itflow-org/develop
Develop to Master for 25.12 release
2025-12-06 14:36:09 -05:00
Johnny
c7ef3627ce Merge pull request #1247 from itflow-org/develop
Merge Develop into Master for v25.11.1 release
2025-11-17 12:22:24 -05:00
Johnny
d1dcc5fb7e Merge pull request #1246 from itflow-org/develop
Develop to Master for Release
2025-11-08 13:47:43 -05:00
55 changed files with 1000 additions and 2180 deletions

View File

@@ -2,21 +2,6 @@
This file documents all notable changes made to ITFlow. 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 ## [25.12] Stable Release
### Breaking Changes ### ### Breaking Changes ###
@@ -334,7 +319,7 @@ We will provide example code with directory structure for each custom directory
--- ---
### Fixed ### Fixed
- Several security vulnerabilities patched (with thanks to www.helx.io). - Several security vulnerabilities patched.
- Ticket status is no longer updated when scheduling. - Ticket status is no longer updated when scheduling.
- Client Portal: Tech contacts can no longer edit their own details. - Client Portal: Tech contacts can no longer edit their own details.
- Fixed overlapping logo issue in Invoice/Quote PDF exports. - Fixed overlapping logo issue in Invoice/Quote PDF exports.

View File

@@ -16,7 +16,7 @@
<br /> <br />
<a href="https://demo.itflow.org"><strong>View demo</strong></a> <a href="https://demo.itflow.org"><strong>View demo</strong></a>
<br /> <br />
Username: <b>demo@demo.com</b> | Password: <b>demo</b> Username: <b>demo@demo</b> | Password: <b>demo</b>
<br /> <br />
<br /> <br />
<a href="https://itflow.org/#about">About</a> <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 | | Version | Supported |
|---------| ------------------ | |---------| ------------------ |
| 25.12 | :white_check_mark: | | 25.05 | :white_check_mark: |
## Reporting a Vulnerability via GitHub Security Advisories ## Reporting a Vulnerability via GitHub Security Advisories

View File

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

View File

@@ -4134,30 +4134,10 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.8'"); mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.8'");
} }
if (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
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 // // Then, update the database to the next sequential version
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.0'"); // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.9'");
// } // }
} else { } else {

View File

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

View File

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

View File

@@ -16,26 +16,11 @@ ob_start();
<div class="modal-body"> <div class="modal-body">
<div class="alert alert-info text-center"> <div class="alert alert-info">
<h6>Before Adding a Payment Provider!</h6> An income account named after the provider will always be created and used for income of paid invoices.<br>
We recommend you add an <strong>Account</strong> and <strong>Vendor</strong> based off the Provider name before continuing eg <strong>Stripe</strong> 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> </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="tab-content">
<div class="tab-pane fade show active" id="pills-details">
<div class="form-group"> <div class="form-group">
<label>Provider <strong class="text-danger">*</strong></label> <label>Provider <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="input-group">
@@ -68,30 +53,6 @@ ob_start();
</div> </div>
</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"> <div class="form-group">
<label>Threshold</label> <label>Threshold</label>
<div class="input-group"> <div class="input-group">
@@ -103,9 +64,7 @@ ob_start();
<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> <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> <hr>
<div class="tab-pane fade" id="pills-expense">
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
@@ -114,60 +73,6 @@ ob_start();
</div> </div>
</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"> <div class="form-group">
<label>Percentage Fee to expense</label> <label>Percentage Fee to expense</label>
<div class="input-group"> <div class="input-group">
@@ -191,8 +96,6 @@ ob_start();
</div> </div>
</div> </div>
</div>
</div>
<div class="modal-footer"> <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> <button type="submit" name="add_payment_provider" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Add</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>

View File

@@ -10,10 +10,10 @@ $row = mysqli_fetch_array($sql);
$provider_name = nullable_htmlentities($row['payment_provider_name']); $provider_name = nullable_htmlentities($row['payment_provider_name']);
$public_key = nullable_htmlentities($row['payment_provider_public_key']); $public_key = nullable_htmlentities($row['payment_provider_public_key']);
$private_key = nullable_htmlentities($row['payment_provider_private_key']); $private_key = nullable_htmlentities($row['payment_provider_private_key']);
$account_id = intval($row['payment_provider_account']); $account_id = nullable_htmlentities($row['payment_provider_account']);
$threshold = floatval($row['payment_provider_threshold']); $threshold = floatval($row['payment_provider_threshold']);
$vendor_id = intval($row['payment_provider_expense_vendor']); $vendor_id = nullable_htmlentities($row['payment_provider_expense_vendor']);
$category_id = intval($row['payment_provider_expense_category']); $category_id = nullable_htmlentities($row['payment_provider_expense_category']);
$percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100; $percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100;
$flat_fee = floatval($row['payment_provider_expense_flat_fee']); $flat_fee = floatval($row['payment_provider_expense_flat_fee']);
@@ -21,39 +21,24 @@ $flat_fee = floatval($row['payment_provider_expense_flat_fee']);
ob_start(); ob_start();
?> ?>
<div class="modal-header bg-dark"> <div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Editing: <strong><?= $provider_name ?></strong></h5> <h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Editing: <strong><?php echo $provider_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal"> <button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span> <span>&times;</span>
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>"> <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
<input type="hidden" name="provider_id" value="<?= $provider_id ?>"> <input type="hidden" name="provider_id" value="<?php echo $provider_id; ?>">
<div class="modal-body"> <div class="modal-body">
<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="tab-content">
<div class="tab-pane fade show active" id="pills-details">
<div class="form-group"> <div class="form-group">
<label>Publishable key <strong class="text-danger">*</strong></label> <label>Publishable key <strong class="text-danger">*</strong></label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div> </div>
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?= $public_key ?>"> <input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?php echo $public_key; ?>">
</div> </div>
</div> </div>
@@ -63,31 +48,7 @@ ob_start();
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span> <span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
</div> </div>
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)" value="<?= $private_key ?>"> <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>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> </div>
@@ -102,64 +63,14 @@ ob_start();
<small class="form-text text-muted">Will not show as an option at Checkout if above this number</small> <small class="form-text text-muted">Will not show as an option at Checkout if above this number</small>
</div> </div>
</div> <hr>
<div class="tab-pane fade" id="pills-expense">
<div class="form-group"> <div class="form-group">
<label>Payment Provider Vendor <strong class="text-danger">*</strong></label> <div class="custom-control custom-switch">
<div class="input-group"> <input type="checkbox" class="custom-control-input" name="enable_expense" <?php if ($vendor_id) { echo "checked"; } ?> value="1" id="enableEditExpenseSwitch">
<div class="input-group-prepend"> <label class="custom-control-label" for="enableEditExpenseSwitch">Enable Expense</label>
<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>
<small>(Category: Payment Processing -- Vendor: <?php echo $provider_name; ?></small>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -183,8 +94,7 @@ ob_start();
</div> </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> <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> </div>
<div class="modal-footer"> <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> <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

@@ -57,7 +57,7 @@ $num_rows = mysqli_num_rows($sql);
<th> <th>
<a class="text-dark">Expensed Fee</a> <a class="text-dark">Expensed Fee</a>
</th> </th>
<th class="text-center"> <th>
<a class="text-dark">Saved Payment Methods</a> <a class="text-dark">Saved Payment Methods</a>
</th> </th>
<th class="text-center">Action</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']); $provider_description = nullable_htmlentities($row['payment_provider_description']);
$account_name = nullable_htmlentities($row['account_name']); $account_name = nullable_htmlentities($row['account_name']);
$threshold = floatval($row['payment_provider_threshold']); $threshold = floatval($row['payment_provider_threshold']);
$vendor_name = nullable_htmlentities($row['vendor_name'] ?? "Expense Disabled"); $vendor_name = nullable_htmlentities($row['vendor_name']);
$category = nullable_htmlentities($row['category_name']); $category = nullable_htmlentities($row['category_name']);
$percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100; $percent_fee = floatval($row['payment_provider_expense_percentage_fee']) * 100;
$flat_fee = floatval($row['payment_provider_expense_flat_fee']); $flat_fee = floatval($row['payment_provider_expense_flat_fee']);
@@ -94,9 +94,7 @@ $num_rows = mysqli_num_rows($sql);
<td><?php echo $vendor_name; ?></td> <td><?php echo $vendor_name; ?></td>
<td><?php echo $category; ?></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 $percent_fee; ?>% + <?php echo numfmt_format_currency($currency_format, $flat_fee, $session_company_currency); ?></td>
<td class="text-center"> <td><?php echo $saved_payment_count; ?></td>
<a class="badge badge-dark badge-pill p-2" href="saved_payment_method.php"><?= $saved_payment_count ?></a>
</td>
<td> <td>
<div class="dropdown dropleft text-center"> <div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown"> <button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">

View File

@@ -14,20 +14,53 @@ if (isset($_POST['add_payment_provider'])) {
$public_key = sanitizeInput($_POST['public_key']); $public_key = sanitizeInput($_POST['public_key']);
$private_key = sanitizeInput($_POST['private_key']); $private_key = sanitizeInput($_POST['private_key']);
$threshold = floatval($_POST['threshold']); $threshold = floatval($_POST['threshold']);
$account = intval($_POST['account']); $enable_expense = intval($_POST['enable_expense'] ?? 0);
$expense_vendor = intval($_POST['expense_vendor']) ?? 0;
$expense_category = intval($_POST['expense_category']) ?? 0;
$percentage_fee = floatval($_POST['percentage_fee']) / 100 ?? 0; $percentage_fee = floatval($_POST['percentage_fee']) / 100 ?? 0;
$flat_fee = floatval($_POST['flat_fee']) ?? 0; $flat_fee = floatval($_POST['flat_fee']) ?? 0;
// Check to ensure provider isn't added twice // Check to ensure provider isn't added twice
$sql = mysqli_query($mysqli, "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1"); $sql = "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1";
if (mysqli_num_rows($sql) > 0) { $result = mysqli_query($mysqli, $sql);
if (mysqli_num_rows($result) > 0) {
flash_alert("Payment Provider <strong>$provider</strong> already exists", 'error'); flash_alert("Payment Provider <strong>$provider</strong> already exists", 'error');
redirect(); redirect();
} }
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"); // 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");
$provider_id = mysqli_insert_id($mysqli); $provider_id = mysqli_insert_id($mysqli);
@@ -48,13 +81,11 @@ if (isset($_POST['edit_payment_provider'])) {
$public_key = sanitizeInput($_POST['public_key']); $public_key = sanitizeInput($_POST['public_key']);
$private_key = sanitizeInput($_POST['private_key']); $private_key = sanitizeInput($_POST['private_key']);
$threshold = floatval($_POST['threshold']); $threshold = floatval($_POST['threshold']);
$account = intval($_POST['account']); $enable_expense = intval($_POST['enable_expense'] ?? 0);
$expense_vendor = intval($_POST['expense_vendor']) ?? 0;
$expense_category = intval($_POST['expense_category']) ?? 0;
$percentage_fee = floatval($_POST['percentage_fee']) / 100; $percentage_fee = floatval($_POST['percentage_fee']) / 100;
$flat_fee = floatval($_POST['flat_fee']); $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_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"); 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");
logAction("Payment Provider", "Edit", "$session_name edited Payment Provider $provider"); logAction("Payment Provider", "Edit", "$session_name edited Payment Provider $provider");

View File

@@ -49,17 +49,3 @@ if (isset($_POST['edit_favicon_settings'])) {
redirect(); 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

@@ -33,16 +33,6 @@ $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 card-dark">
<div class="card-header"> <div class="card-header">
<h3 class="card-title"><i class="fas fa-fw fa-credit-card mr-2"></i>Saved Payment Methods</h3> <h3 class="card-title"><i class="fas fa-fw fa-credit-card mr-2"></i>Saved Payment Methods</h3>
@@ -117,16 +107,8 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?> ?>
<tr> <tr>
<td> <td><?php echo $client_name; ?> (<?php echo $client_id; ?>)</td>
<?= $client_name ?> <td><?php echo $provider_name; ?> (<?php echo $provider_id; ?>)</td>
<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 $saved_payment_description; ?></td>
<td><?php echo $provider_client; ?></td> <td><?php echo $provider_client; ?></td>
<td><?php echo $provider_payment_method; ?></td> <td><?php echo $provider_payment_method; ?></td>

View File

@@ -57,12 +57,11 @@ require_once "includes/inc_all_admin.php";
<hr> <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> <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> </form>
</div> </div>
</div> </div>
<?php <?php
require_once "../includes/footer.php"; require_once "../includes/footer.php";

View File

@@ -195,7 +195,7 @@ if (isset($_GET['share_generate_link'])) {
$item_expires_friendly = "1 month"; $item_expires_friendly = "1 month";
} }
$item_key = randomString(32); $item_key = randomString(156);
if ($item_type == "Document") { 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")); $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"));
@@ -992,23 +992,3 @@ if (isset($_GET['apex_domain_check'])) {
echo json_encode($response); 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

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

View File

@@ -797,9 +797,7 @@ if (isset($_GET['contact_id'])) {
<div class="card-header py-2"> <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> <h3 class="card-title mt-2"><i class="fa fa-fw fa-life-ring mr-2"></i>Related Tickets</h3>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addTicketModal">
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 <i class="fas fa-plus mr-2"></i>New Ticket
</button> </button>
</div> </div>

View File

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

View File

@@ -463,25 +463,32 @@ ob_start();
</div> </div>
</form> </form>
<!-- Ticket Templates -->
<script> <script>
$(document).on('change', '#ticket_template_select', function () { document.addEventListener("DOMContentLoaded", function() {
const $opt = $(this).find(':selected'); var templateSelect = $('#ticket_template_select');
const templateSubject = $opt.data('subject') || ''; var subjectInput = document.getElementById('subjectInput');
const templateDetails = $opt.data('details') || ''; var detailsInput = document.getElementById('detailsInput');
$('#subjectInput').val(templateSubject); templateSelect.on('select2:select', function(e) {
var selectedOption = e.params.data.element;
var templateSubject = selectedOption.getAttribute('data-subject');
var templateDetails = selectedOption.getAttribute('data-details');
if (window.tinymce) { // Update Subject
const editor = tinymce.get('detailsInput'); subjectInput.value = templateSubject || '';
// Update Details
if (typeof tinymce !== 'undefined') {
var editor = tinymce.get('detailsInput');
if (editor) { if (editor) {
editor.setContent(templateDetails); editor.setContent(templateDetails || '');
} else { } else {
$('#detailsInput').val(templateDetails); detailsInput.value = templateDetails || '';
} }
} else { } else {
$('#detailsInput').val(templateDetails); detailsInput.value = templateDetails || '';
} }
});
}); });
</script> </script>

View File

@@ -18,7 +18,7 @@ ob_start();
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<!-- Hidden/System fields --> <!-- Hidden/System fields -->
<?php if ($client_id) { ?> <?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 } ?>
<?php if ($project_id) { ?> <?php if ($project_id) { ?>
<input type="hidden" name="project" value="<?php echo $project_id; ?>"> <input type="hidden" name="project" value="<?php echo $project_id; ?>">
@@ -297,24 +297,32 @@ ob_start();
<!-- Ticket Templates --> <!-- Ticket Templates -->
<script> <script>
$(document).on('change', '#ticket_template_select', function () { document.addEventListener("DOMContentLoaded", function() {
const $opt = $(this).find(':selected'); var templateSelect = $('#ticket_template_select');
const templateSubject = $opt.data('subject') || ''; var subjectInput = document.getElementById('subjectInput');
const templateDetails = $opt.data('details') || ''; var detailsInput = document.getElementById('detailsInput');
$('#subjectInput').val(templateSubject); templateSelect.on('select2:select', function(e) {
var selectedOption = e.params.data.element;
var templateSubject = selectedOption.getAttribute('data-subject');
var templateDetails = selectedOption.getAttribute('data-details');
if (window.tinymce) { // Update Subject
const editor = tinymce.get('detailsInput'); subjectInput.value = templateSubject || '';
// Update Details
if (typeof tinymce !== 'undefined') {
var editor = tinymce.get('detailsInput');
if (editor) { if (editor) {
editor.setContent(templateDetails); editor.setContent(templateDetails || '');
} else { } else {
$('#detailsInput').val(templateDetails); detailsInput.value = templateDetails || '';
} }
} else { } else {
$('#detailsInput').val(templateDetails); detailsInput.value = templateDetails || '';
} }
}); });
});
</script> </script>
<!-- Ticket Client/Contact JS --> <!-- Ticket Client/Contact JS -->

View File

@@ -1,140 +0,0 @@
<?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,14 +14,6 @@ $task_name = nullable_htmlentities($row['task_name']);
$task_completion_estimate = intval($row['task_completion_estimate']); $task_completion_estimate = intval($row['task_completion_estimate']);
$task_completed_at = nullable_htmlentities($row['task_completed_at']); $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. // Generate the HTML form content using output buffering.
ob_start(); ob_start();
@@ -58,52 +50,6 @@ ob_start();
</div> </div>
</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>
<div class="modal-footer"> <div class="modal-footer">

View File

@@ -794,7 +794,7 @@ if (isset($_POST['bulk_add_client_ticket'])) {
$config_base_url = sanitizeInput($config_base_url); $config_base_url = sanitizeInput($config_base_url);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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); $invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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); $new_invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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,13 +570,15 @@ if (isset($_GET['email_invoice'])) {
} }
// Queue Mail // Queue Mail
$data[] = [ $data = [
[
'from' => $config_invoice_from_email, 'from' => $config_invoice_from_email,
'from_name' => $config_invoice_from_name, 'from_name' => $config_invoice_from_name,
'recipient' => $contact_email, 'recipient' => $contact_email,
'recipient_name' => $contact_name, 'recipient_name' => $contact_name,
'subject' => $subject, 'subject' => $subject,
'body' => $body 'body' => $body
]
]; ];
addToMailQueue($data); addToMailQueue($data);
@@ -611,13 +613,15 @@ if (isset($_GET['email_invoice'])) {
$billing_contact_name = sanitizeInput($billing_contact['contact_name']); $billing_contact_name = sanitizeInput($billing_contact['contact_name']);
$billing_contact_email = sanitizeInput($billing_contact['contact_email']); $billing_contact_email = sanitizeInput($billing_contact['contact_email']);
$data[] = [ $data = [
[
'from' => $config_invoice_from_email, 'from' => $config_invoice_from_email,
'from_name' => $config_invoice_from_name, 'from_name' => $config_invoice_from_name,
'recipient' => $billing_contact_email, 'recipient' => $billing_contact_email,
'recipient_name' => $billing_contact_name, 'recipient_name' => $billing_contact_name,
'subject' => $subject, 'subject' => $subject,
'body' => $body '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); logAction("Invoice", "Email", "$session_name Emailed $billing_contact_email Invoice $invoice_prefix$invoice_number Email queued Email ID: $email_id", $client_id, $invoice_id);
@@ -655,7 +659,7 @@ if (isset($_POST['export_invoices_csv'])) {
$file_name_date = date('Y-m-d_H-i-s'); $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 AND $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 $client_query ORDER BY invoice_number ASC");
$num_rows = mysqli_num_rows($sql); $num_rows = mysqli_num_rows($sql);

View File

@@ -26,7 +26,7 @@ if (isset($_POST['add_quote'])) {
$quote_number = mysqli_insert_id($mysqli); $quote_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$quote_url_key = randomString(32); $quote_url_key = randomString(156);
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"); 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']); $category_id = intval($row['quote_category_id']);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$quote_url_key = randomString(32); $quote_url_key = randomString(156);
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"); 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); $invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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); $new_invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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']); $client_id = intval($row['recurring_ticket_client_id']);
$asset_id = intval($row['recurring_ticket_asset_id']); $asset_id = intval($row['recurring_ticket_asset_id']);
$category = intval($row['recurring_ticket_category']); $category = intval($row['recurring_ticket_category']);
$url_key = randomString(32); $url_key = randomString(156);
$ticket_status = 1; // Default $ticket_status = 1; // Default
if ($assigned_id > 0) { if ($assigned_id > 0) {
@@ -228,7 +228,7 @@ if (isset($_GET['force_recurring_ticket'])) {
$client_id = intval($row['recurring_ticket_client_id']); $client_id = intval($row['recurring_ticket_client_id']);
$asset_id = intval($row['recurring_ticket_asset_id']); $asset_id = intval($row['recurring_ticket_asset_id']);
$category = intval($row['recurring_ticket_category']); $category = intval($row['recurring_ticket_category']);
$url_key = randomString(32); $url_key = randomString(156);
$ticket_status = 1; // Default $ticket_status = 1; // Default
if ($assigned_id > 0) { if ($assigned_id > 0) {

View File

@@ -155,247 +155,6 @@ 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'])) { if (isset($_GET['complete_all_tasks'])) {
enforceUserPermission('module_support', 2); enforceUserPermission('module_support', 2);

View File

@@ -68,7 +68,7 @@ if (isset($_POST['add_ticket'])) {
$config_base_url = sanitizeInput($config_base_url); $config_base_url = sanitizeInput($config_base_url);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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); $config_base_url = sanitizeInput($config_base_url);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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); $invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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); $invoice_id = mysqli_insert_id($mysqli);

View File

@@ -961,82 +961,23 @@ if (isset($_GET['ticket_id'])) {
<table class="table table-sm" id="tasks"> <table class="table table-sm" id="tasks">
<?php <?php
while ($row = mysqli_fetch_array($sql_tasks)) { while($row = mysqli_fetch_array($sql_tasks)){
$task_id = intval($row['task_id']); $task_id = intval($row['task_id']);
$task_name = nullable_htmlentities($row['task_name']); $task_name = nullable_htmlentities($row['task_name']);
//$task_description = nullable_htmlentities($row['task_description']); // not in db yet //$task_description = nullable_htmlentities($row['task_description']); // not in db yet
$task_completion_estimate = intval($row['task_completion_estimate']); $task_completion_estimate = intval($row['task_completion_estimate']);
$task_completed_at = nullable_htmlentities($row['task_completed_at']); $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 ?>"> <tr data-task-id="<?= $task_id ?>">
<td> <td>
<?php if ($task_completed_at) { ?> <?php if ($task_completed_at) { ?>
<i class="far fa-check-square text-success"></i> <i class="far fa-check-square text-success"></i>
<?php } elseif (lookupUserPermission("module_support") >= 2) { ?> <?php } elseif (lookupUserPermission("module_support") >= 2) { ?>
<?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; ?>"> <a href="post.php?complete_task=<?php echo $task_id; ?>">
<i class="far fa-square text-dark"></i> <i class="far fa-square text-dark"></i>
</a> </a>
<?php } ?>
<span class="text-dark ml-2"><?php echo $task_name; ?></span> <span class="text-dark ml-2"><?php echo $task_name; ?></span>
<?php } ?>
<?php } ?>
</td> </td>
<td> <td>
<div class="float-right"> <div class="float-right">
@@ -1056,12 +997,6 @@ if (isset($_GET['ticket_id'])) {
data-modal-url="modals/ticket/ticket_task_edit.php?id=<?= $task_id ?>"> data-modal-url="modals/ticket/ticket_task_edit.php?id=<?= $task_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit <i class="fas fa-fw fa-edit mr-2"></i>Edit
</a> </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) { ?> <?php if ($task_completed_at) { ?>
<a class="dropdown-item" href="post.php?undo_complete_task=<?php echo $task_id; ?>"> <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 <i class="fas fa-fw fa-arrow-circle-left mr-2"></i>Mark incomplete

View File

@@ -39,7 +39,7 @@
</th> </th>
<?php if ($config_module_enable_accounting && lookupUserPermission("module_sales") >= 2) { ?> <?php if ($config_module_enable_accounting && lookupUserPermission("module_sales") >= 2) { ?>
<th class="text-center"> <th class="text-center">
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ticket_billable&order=<?= $disp ?>"> <a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ticket_billable&order=<?php echo $disp; ?>">
Billable <?php if ($sort == 'ticket_billable') { echo $order_icon; } ?> Billable <?php if ($sort == 'ticket_billable') { echo $order_icon; } ?>
</a> </a>
</th> </th>
@@ -242,9 +242,9 @@
data-modal-url="modals/ticket/ticket_billable.php?id=<?= $ticket_id ?>"> data-modal-url="modals/ticket/ticket_billable.php?id=<?= $ticket_id ?>">
<?php <?php
if ($ticket_billable == 1) { if ($ticket_billable == 1) {
echo "<span class='badge badge-pill badge-success p-2'><i class='fas fa-fw fa-check'></i></span>"; echo "<span class='badge badge-pill badge-success p-2'>Yes</span>";
} else { } else {
echo "<span class='badge badge-pill badge-secondary p-2'><i class='fas fa-fw fa-minus'></i></span>"; echo "<span class='badge badge-pill badge-secondary p-2'>No</span>";
} }
?> ?>
</a> </a>

View File

@@ -51,3 +51,4 @@ $data = "otpauth://totp/ITFlow:$session_email?secret=$token";
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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) // Enable extension access, only if it isn't already setup (user doesn't have cookie)
if (isset($_POST['extension']) && $_POST['extension'] == 'Yes') { if (isset($_POST['extension']) && $_POST['extension'] == 'Yes') {
if (!isset($_COOKIE['user_extension_key'])) { if (!isset($_COOKIE['user_extension_key'])) {
$extension_key = randomString(32); $extension_key = randomString(156);
mysqli_query($mysqli, "UPDATE users SET user_extension_key = '$extension_key' WHERE user_id = $session_user_id"); mysqli_query($mysqli, "UPDATE users SET user_extension_key = '$extension_key' WHERE user_id = $session_user_id");
$extended_log_description .= "enabled browser extension access"; $extended_log_description .= "enabled browser extension access";

View File

@@ -44,7 +44,7 @@ if (!empty($subject)) {
$ticket_number = mysqli_insert_id($mysqli); $ticket_number = mysqli_insert_id($mysqli);
// Insert ticket // Insert ticket
$url_key = randomString(32); $url_key = randomString(156);
$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"); $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 // Check insert & get insert ID

View File

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

234
client/login.php Normal file
View File

@@ -0,0 +1,234 @@
<?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

@@ -12,7 +12,7 @@ require_once '../includes/load_global_settings.php';
if (empty($config_smtp_host)) { if (empty($config_smtp_host)) {
header("Location: /login.php"); header("Location: login.php");
exit(); exit();
} }
@@ -72,7 +72,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") {
$name = sanitizeInput($row['contact_name']); $name = sanitizeInput($row['contact_name']);
$client = intval($row['contact_client_id']); $client = intval($row['contact_client_id']);
$token = randomString(32); $token = randomString(156);
$url = "https://$config_base_url/client/login_reset.php?email=$email&token=$token&client=$client"; $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, "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"); 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 // Redirect to login page
$_SESSION['login_message'] = "Password reset successfully!"; $_SESSION['login_message'] = "Password reset successfully!";
header("Location: /login.php"); header("Location: login.php");
exit(); exit();
} else { } else {
@@ -275,7 +275,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") {
?> ?>
</p> </p>
<a href="/login.php">Back to login</a> <a href="login.php">Back to login</a>
</div> </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); $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 //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
// Ensure priority is low/med/high (as can be user defined) // Ensure priority is low/med/high (as can be user defined)
if ($_POST['priority'] !== "Low" && $_POST['priority'] !== "Medium" && $_POST['priority'] !== "High") { if ($_POST['priority'] !== "Low" && $_POST['priority'] !== "Medium" && $_POST['priority'] !== "High") {
@@ -185,43 +185,6 @@ 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'])) { if (isset($_POST['add_ticket_feedback'])) {
$ticket_id = intval($_POST['ticket_id']); $ticket_id = intval($_POST['ticket_id']);
@@ -357,7 +320,7 @@ if (isset($_GET['logout'])) {
session_unset(); session_unset();
session_destroy(); 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 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 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> <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>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 --> <!-- // Show option to change password if auth provider is local -->

View File

@@ -70,13 +70,6 @@ if (isset($_GET['id']) && intval($_GET['id'])) {
); );
$completed_task_count = mysqli_num_rows($sql_tasks_completed); $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"> <ol class="breadcrumb d-print-none">
@@ -137,59 +130,6 @@ if (isset($_GET['id']) && intval($_GET['id'])) {
</div> </div>
</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> <hr>
<!-- Either show the reply comments box, option to re-open ticket, show ticket smiley feedback or thanks for feedback --> <!-- 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); $new_invoice_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access //Generate a unique URL key for clients to access
$url_key = randomString(32); $url_key = randomString(156);
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"); 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); $contact_email_esc = mysqli_real_escape_string($mysqli, $contact_email);
$client_id = intval($client_id); $client_id = intval($client_id);
$url_key = randomString(32); $url_key = randomString(156);
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"); 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); $id = mysqli_insert_id($mysqli);

19
db.sql
View File

@@ -2441,25 +2441,6 @@ CREATE TABLE `tasks` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `taxes`
-- --

View File

@@ -4,13 +4,20 @@
DEFINE("WORDING_ROLECHECK_FAILED", "You are not permitted to do that!"); DEFINE("WORDING_ROLECHECK_FAILED", "You are not permitted to do that!");
// Function to generate both crypto & URL safe random strings // Function to generate both crypto & URL safe random strings
function randomString(int $length = 16): string { function randomString($length = 16) {
$bytes = random_bytes((int) ceil($length * 3 / 4)); // Generate some cryptographically safe random bytes
return substr( // Generate a little more than requested as we'll lose some later converting
rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='), $random_bytes = random_bytes($length + 5);
0,
$length // 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);
} }
// Older keygen function - only used for TOTP currently // Older keygen function - only used for TOTP currently
@@ -1436,10 +1443,6 @@ function appNotify($type, $details, $action = null, $client_id = 0, $entity_id =
function logAction($type, $action, $description, $client_id = 0, $entity_id = 0) { function logAction($type, $action, $description, $client_id = 0, $entity_id = 0) {
global $mysqli, $session_user_agent, $session_ip, $session_user_id; 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)) { if (empty($session_user_id)) {
$session_user_id = 0; $session_user_id = 0;
} }
@@ -1781,220 +1784,3 @@ 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

@@ -1,114 +0,0 @@
<?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,39 +225,6 @@ 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'])) { if (isset($_GET['export_quote_pdf'])) {
$quote_id = intval($_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. * 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.1"); DEFINE("APP_VERSION", "25.12");

View File

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

630
login.php
View File

@@ -1,78 +1,68 @@
<?php <?php
// Unified login (Agent + Client) using one email & password // Enforce a Content Security Policy for security against cross-site scripting
header("Content-Security-Policy: default-src 'self'"); header("Content-Security-Policy: default-src 'self'");
// Check if the config.php file exists
if (!file_exists('config.php')) { if (!file_exists('config.php')) {
header("Location: /setup"); // Redirect to the setup page if config.php doesn't exist
header("Location: /setup"); // Must use header as functions aren't included yet
exit(); exit();
} }
require_once "config.php"; require_once "config.php";
require_once "functions.php";
require_once "plugins/totp/totp.php";
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();
}
// Check if setup mode is enabled or the variable is missing
if (!isset($config_enable_setup) || $config_enable_setup == 1) { if (!isset($config_enable_setup) || $config_enable_setup == 1) {
// Redirect to the setup page
header("Location: /setup"); header("Location: /setup");
exit(); exit();
} }
if ( // Set Timezone
$config_https_only require_once "includes/inc_set_timezone.php";
&& (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on')
&& (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https') // 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')) {
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."; 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; exit;
} }
require_once "includes/inc_set_timezone.php"; require_once "functions.php";
require_once "plugins/totp/totp.php";
// IP & User Agent for logging
$session_ip = sanitizeInput(getIP()); $session_ip = sanitizeInput(getIP());
$session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT'] ?? ''); $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)"));
$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']); $failed_login_count = intval($row['failed_login_count']);
if ($failed_login_count >= 15) { if ($failed_login_count >= 15) {
logAction("Login", "Blocked", "$session_ip was blocked access to login due to IP lockout"); 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"); 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."); 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.");
} }
// Settings // Query Settings for company
$sql_settings = mysqli_query($mysqli, " $sql_settings = mysqli_query($mysqli, "SELECT * FROM settings LEFT JOIN companies ON settings.company_id = companies.company_id WHERE settings.company_id = 1");
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); $row = mysqli_fetch_array($sql_settings);
// Company info
$company_name = $row['company_name']; $company_name = $row['company_name'];
$company_logo = $row['company_logo']; $company_logo = $row['company_logo'];
$config_start_page = nullable_htmlentities($row['config_start_page']); $config_start_page = nullable_htmlentities($row['config_start_page']);
$config_login_message = nullable_htmlentities($row['config_login_message']); $config_login_message = nullable_htmlentities($row['config_login_message']);
// Mail
$config_smtp_host = $row['config_smtp_host']; $config_smtp_host = $row['config_smtp_host'];
$config_smtp_port = intval($row['config_smtp_port']); $config_smtp_port = intval($row['config_smtp_port']);
$config_smtp_encryption = $row['config_smtp_encryption']; $config_smtp_encryption = $row['config_smtp_encryption'];
@@ -81,266 +71,78 @@ $config_smtp_password = $row['config_smtp_password'];
$config_mail_from_email = sanitizeInput($row['config_mail_from_email']); $config_mail_from_email = sanitizeInput($row['config_mail_from_email']);
$config_mail_from_name = sanitizeInput($row['config_mail_from_name']); $config_mail_from_name = sanitizeInput($row['config_mail_from_name']);
// Client Portal Enabled
$config_client_portal_enable = intval($row['config_client_portal_enable']); $config_client_portal_enable = intval($row['config_client_portal_enable']);
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
// Login key (if setup)
$config_login_key_required = $row['config_login_key_required']; $config_login_key_required = $row['config_login_key_required'];
$config_login_key_secret = $row['config_login_key_secret']; $config_login_key_secret = $row['config_login_key_secret'];
$azure_client_id = $row['config_azure_client_id'] ?? null; $config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
$response = null; // Login key verification
$token_field = null; // If no/incorrect 'key' is supplied, send to client portal instead
$show_role_choice = false; if ($config_login_key_required) {
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
$email = ''; redirect("client");
$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);
} }
// POST handling // HTTP-Only cookies
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']) || isset($_POST['mfa_login']))) { ini_set("session.cookie_httponly", true);
$role_choice = $_POST['role_choice'] ?? null; // Tell client to only send cookie(s) over HTTPS
if ($config_https_only || !isset($config_https_only)) {
ini_set("session.cookie_secure", true);
}
$is_login_step = isset($_POST['login']); // Handle POST login request
$is_role_step = isset($_POST['role_choice']) && !$is_login_step && !isset($_POST['mfa_login']); if (isset($_POST['login'])) {
$is_mfa_step = isset($_POST['mfa_login']);
// ----------------------------------- // Sessions should start after the user has POSTed data
// STEP 2: ROLE CHOICE (no email/pass) session_start();
// -----------------------------------
if ($is_role_step) {
$posted_token = $_POST['pending_login_token'] ?? ''; // Passed login brute force check
$sess = $_SESSION['pending_dual_login'] ?? null; $email = sanitizeInput($_POST['email']);
$password = $_POST['password'];
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) { $current_code = 0; // Default value
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 {
$email = sanitizeInput($sess['email'] ?? '');
}
}
// -----------------------------------
// STEP 3: MFA SUBMIT (no email/pass)
// -----------------------------------
if ($is_mfa_step && empty($response)) {
$posted_token = $_POST['pending_mfa_token'] ?? '';
$sess = $_SESSION['pending_mfa_login'] ?? null;
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';
}
}
// -----------------------------------
// STEP 1: INITIAL CREDENTIALS
// -----------------------------------
if ($is_login_step && empty($response)) {
$email = sanitizeInput($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
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.
</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'])) { if (isset($_POST['current_code'])) {
$current_code = intval($_POST['current_code']); $current_code = intval($_POST['current_code']);
} }
$mfa_is_complete = false; $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"));
$extended_log = '';
// 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)) { if (empty($token)) {
$mfa_is_complete = true; // no MFA configured // MFA is not configured
$mfa_is_complete = true;
} }
// remember-me cookie allows bypass // Validate MFA via a remember-me cookie
if (isset($_COOKIE['rememberme'])) { if (isset($_COOKIE['rememberme'])) {
$remember_tokens = mysqli_query($mysqli, " // Get remember tokens less than $config_login_remember_me_days_expire days old
SELECT remember_token_token $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)");
FROM remember_tokens while ($row = mysqli_fetch_assoc($remember_tokens)) {
WHERE remember_token_user_id = $user_id if (hash_equals($row['remember_token_token'], $_COOKIE['rememberme'])) {
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; $mfa_is_complete = true;
$extended_log = 'with 2FA remember-me cookie'; $extended_log = 'with 2FA remember-me cookie';
break; break;
@@ -355,130 +157,92 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
} }
if ($mfa_is_complete) { if ($mfa_is_complete) {
// MFA Completed successfully
// Clear pending MFA if exists // FULL LOGIN SUCCESS
unset($_SESSION['pending_mfa_login']);
// Remember me token creation // Create a remember me token, if requested
if (isset($_POST['remember_me'])) { 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)); $newRememberToken = bin2hex(random_bytes(64));
setcookie('rememberme', $newRememberToken, time() + 86400 * $config_login_remember_me_expire, "/", null, true, true); 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'");
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"; $extended_log .= ", generated a new remember-me token";
} }
// Suspicious login checks / email notify (kept from your code) // Check this login isn't suspicious
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, " $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"));
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']); $ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']);
$sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, " $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"));
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']); $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) { if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
$subject = "$config_app_name new login for $user_name"; $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"; $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 = [[ $data = [
[
'from' => $config_mail_from_email, 'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name, 'from_name' => $config_mail_from_name,
'recipient' => $user_email, 'recipient' => $user_email,
'recipient_name' => $user_name, 'recipient_name' => $user_name,
'subject' => $subject, 'subject' => $subject,
'body' => $body 'body' => $body
]]; ]
];
addToMailQueue($data); addToMailQueue($data);
} }
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id); logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
// Session info
$_SESSION['user_id'] = $user_id; $_SESSION['user_id'] = $user_id;
$_SESSION['csrf_token'] = randomString(32); $_SESSION['csrf_token'] = randomString(156);
$_SESSION['logged'] = true; $_SESSION['logged'] = true;
// Forcing MFA
if ($force_mfa == 1 && $token == NULL) { if ($force_mfa == 1 && $token == NULL) {
$config_start_page = "user/mfa_enforcement.php"; $config_start_page = "user/mfa_enforcement.php";
} }
// Setup encryption session key WITHOUT PASSWORD IN SESSION // Setup encryption session key
// If we are coming from MFA step, master key is in pending_mfa_login. if (isset($user_encryption_ciphertext)) {
// 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); $site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
}
}
if (!empty($site_encryption_master_key)) {
generateUserSessionKey($site_encryption_master_key); 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 // 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'))) { 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']));
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']) );
} else { } else {
redirect("agent/$config_start_page"); redirect("agent/$config_start_page");
} }
} else { } else {
// MFA required — store *only what we need*, not password // MFA is configured and needs to be confirmed, or was unsuccessful
$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()
];
// HTML code for the token input field
$token_field = " $token_field = "
<div class='input-group mb-3'> <div class='input-group mb-3'>
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6' <input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6' class='form-control' placeholder='Enter your 2FA code' name='current_code' required autofocus>
class='form-control' placeholder='Verify your 2FA code'
name='current_code' required autofocus>
<div class='input-group-append'> <div class='input-group-append'>
<div class='input-group-text'> <div class='input-group-text'>
<span class='fas fa-key'></span> <span class='fas fa-key'></span>
@@ -486,101 +250,80 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
</div> </div>
</div>"; </div>";
// Log/notify if MFA was unsuccessful
if ($current_code !== 0) { if ($current_code !== 0) {
logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id);
// 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)) { if (!empty($config_smtp_host)) {
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name"; $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"; $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 = [[ $data = [
[
'from' => $config_mail_from_email, 'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name, 'from_name' => $config_mail_from_name,
'recipient' => $user_email, 'recipient' => $user_email,
'recipient_name' => $user_name, 'recipient_name' => $user_name,
'subject' => $subject, 'subject' => $subject,
'body' => $body 'body' => $body
]]; ]
addToMailQueue($data); ];
$mail = addToMailQueue($data);
} }
// HTML feedback for incorrect 2FA code
$response = " $response = "
<div class='alert alert-danger'> <div class='alert alert-warning'>
Please enter a valid 2FA code. Please Enter 2FA Code!
<button class='close' data-dismiss='alert'>&times;</button>
</div>"; </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 { } else {
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id); // Password incorrect or user doesn't exist - show generic error
header("HTTP/1.1 401 Unauthorized"); header("HTTP/1.1 401 Unauthorized");
logAction("Login", "Failed", "Failed login attempt using $email");
$response = " $response = "
<div class='alert alert-danger'> <div class='alert alert-danger'>
Incorrect username or password. Incorrect username or password.
<button class='close' data-dismiss='alert'>&times;</button>
</div>"; </div>";
} }
}
}
}
}
}
} }
// Form state
$show_mfa_form = (isset($token_field) && !empty($token_field));
$show_login_form = (!$show_role_choice && !$show_mfa_form);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title><?php echo nullable_htmlentities($company_name); ?> | Login</title> <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="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<!-- Font Awesome -->
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css"> <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')) { ?> <?php if(file_exists('uploads/favicon.ico')) { ?>
<link rel="icon" type="image/x-icon" href="/uploads/favicon.ico"> <link rel="icon" type="image/x-icon" href="/uploads/favicon.ico">
<?php } ?> <?php } ?>
<!-- Theme style -->
<link rel="stylesheet" href="plugins/adminlte/css/adminlte.min.css"> <link rel="stylesheet" href="plugins/adminlte/css/adminlte.min.css">
</head> </head>
<body class="hold-transition login-page"> <body class="hold-transition login-page">
@@ -593,6 +336,7 @@ $show_login_form = (!$show_role_choice && !$show_mfa_form);
<?php } ?> <?php } ?>
</div> </div>
<!-- /.login-logo -->
<div class="card"> <div class="card">
<div class="card-body login-card-body"> <div class="card-body login-card-body">
@@ -606,24 +350,16 @@ $show_login_form = (!$show_role_choice && !$show_mfa_form);
<form method="post"> <form method="post">
<?php if ($show_login_form): ?> <div class="input-group mb-3" <?php if (isset($token_field)) { echo "hidden"; } ?>>
<!-- STEP 1: Email + Password --> <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 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-append">
<div class="input-group-text"> <div class="input-group-text">
<span class="fas fa-envelope"></span> <span class="fas fa-envelope"></span>
</div> </div>
</div> </div>
</div> </div>
<div class="input-group mb-3" <?php if (isset($token_field)) { echo "hidden"; } ?>>
<div class="input-group mb-3"> <input type="password" class="form-control" placeholder="Agent Password" name="password" value="<?php if (isset($token_field)) { echo $password; } ?>" required>
<input type="password" class="form-control" placeholder="Password" name="password" required>
<div class="input-group-append"> <div class="input-group-append">
<div class="input-group-text"> <div class="input-group-text">
<span class="fas fa-lock"></span> <span class="fas fa-lock"></span>
@@ -631,30 +367,11 @@ $show_login_form = (!$show_role_choice && !$show_mfa_form);
</div> </div>
</div> </div>
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button> <?php
<?php endif; ?> if (isset($token_field)) {
<?php if ($show_role_choice): ?> echo $token_field;
<!-- 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="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>
<?php endif; ?>
<?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="form-group mb-3">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
@@ -663,38 +380,39 @@ $show_login_form = (!$show_role_choice && !$show_mfa_form);
</div> </div>
</div> </div>
<button type="submit" class="btn btn-dark btn-block mb-3" name="mfa_login">Verify & Sign In</button> <?php
<?php endif; ?>
</form> }
?>
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
<?php if($config_client_portal_enable == 1){ ?> <?php if($config_client_portal_enable == 1){ ?>
<hr> <hr>
<?php if (!empty($config_smtp_host)) { ?> <h5 class="text-center">Looking for the <a href="client">Client Portal?<a/></h5>
<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 } ?> <?php } ?>
</form>
</div> </div>
<!-- /.login-card-body -->
</div> </div>
</div> </div>
<!-- /.login-box -->
<?php <!-- jQuery -->
if (!$config_whitelabel_enabled) {
echo '<small class="text-muted">Powered by ITFlow</small>';
}
?>
<script src="plugins/jquery/jquery.min.js"></script> <script src="plugins/jquery/jquery.min.js"></script>
<!-- Bootstrap 4 -->
<script src="plugins/bootstrap/js/bootstrap.bundle.min.js"></script> <script src="plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="plugins/adminlte/js/adminlte.min.js"></script> <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> <script src="js/login_prevent_resubmit.js"></script>
</body> </body>