mirror of
https://github.com/itflow-org/itflow
synced 2026-03-13 17:24:51 +00:00
Compare commits
30 Commits
v25.12
...
ticket-tas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58bcb38617 | ||
|
|
77e4d2b566 | ||
|
|
88a29b7599 | ||
|
|
64525750b6 | ||
|
|
30499123f1 | ||
|
|
79703042ff | ||
|
|
ccd5605d97 | ||
|
|
908277065b | ||
|
|
f2d4eb0486 | ||
|
|
f784b659e8 | ||
|
|
e60a7a59f9 | ||
|
|
cab81ca170 | ||
|
|
a82e2c7ea1 | ||
|
|
a277380441 | ||
|
|
ad5710b1d8 | ||
|
|
3e3531a6ce | ||
|
|
a79ce23ae5 | ||
|
|
32f996d034 | ||
|
|
312eb4dffc | ||
|
|
1916456c84 | ||
|
|
9b8d37b577 | ||
|
|
05018e5f17 | ||
|
|
72ef918452 | ||
|
|
27fde82aff | ||
|
|
b27ffe6635 | ||
|
|
84cc4a094a | ||
|
|
e75600ee05 | ||
|
|
871ad2ea7e | ||
|
|
8b5f2e0f3f | ||
|
|
58d6ab7342 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
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 ###
|
||||||
@@ -319,7 +334,7 @@ We will provide example code with directory structure for each custom directory
|
|||||||
---
|
---
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Several security vulnerabilities patched.
|
- Several security vulnerabilities patched (with thanks to www.helx.io).
|
||||||
- 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.
|
||||||
|
|||||||
@@ -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</b> | Password: <b>demo</b>
|
Username: <b>demo@demo.com</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>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ We operate a rolling release model. Any bug fixes will be released into latest v
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|---------| ------------------ |
|
|---------| ------------------ |
|
||||||
| 25.05 | :white_check_mark: |
|
| 25.12 | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability via GitHub Security Advisories
|
## Reporting a Vulnerability via GitHub Security Advisories
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ $num_rows = mysqli_num_rows($sql);
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<ol class="breadcrumb d-print-none">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="/admin">Admin</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="ai_provider.php">AI Providers</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active">AI Models</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
<div class="card card-dark">
|
<div class="card 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>
|
||||||
|
|||||||
@@ -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>
|
<th class="text-center">
|
||||||
<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,7 +67,8 @@ $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><?php echo $ai_model_count; ?></td>
|
<td class="text-center">
|
||||||
|
<a class="badge badge-dark badge-pill p-2" href="ai_model.php"><?= $ai_model_count ?></a>
|
||||||
<td>
|
<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">
|
||||||
|
|||||||
@@ -4134,10 +4134,30 @@ 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.3.9'");
|
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.0'");
|
||||||
// }
|
// }
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -69,30 +69,20 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="/admin/payment_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'payment_provider.php' ? 'active' : ''); ?>">
|
<a href="/admin/payment_provider.php"
|
||||||
|
class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['payment_provider.php', 'saved_payment_method.php']) ? 'active' : ''); ?>">
|
||||||
<i class="nav-icon far fa-credit-card"></i>
|
<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" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ai_provider.php' ? 'active' : ''); ?>">
|
<a href="/admin/ai_provider.php"
|
||||||
|
class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['ai_provider.php', 'ai_model.php']) ? 'active' : ''); ?>">
|
||||||
<i class="nav-icon fas fa-robot"></i>
|
<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">
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
require_once '../../../includes/modal_header.php';
|
require_once '../../../includes/modal_header.php';
|
||||||
|
|
||||||
$key = randomString(156);
|
$key = randomString(32);
|
||||||
$decryptPW = randomString(160);
|
$decryptPW = randomString(32);
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -16,85 +16,182 @@ ob_start();
|
|||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info text-center">
|
||||||
An income account named after the provider will always be created and used for income of paid invoices.<br>
|
<h6>Before Adding a Payment Provider!</h6>
|
||||||
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.
|
We recommend you add an <strong>Account</strong> and <strong>Vendor</strong> based off the Provider name before continuing eg <strong>Stripe</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<ul class="nav nav-pills nav-justified mb-3">
|
||||||
<label>Provider <strong class="text-danger">*</strong></label>
|
<li class="nav-item">
|
||||||
<div class="input-group">
|
<a class="nav-link active" data-toggle="pill" href="#pills-details">Details</a>
|
||||||
<div class="input-group-prepend">
|
</li>
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-credit-card"></i></span>
|
<li class="nav-item">
|
||||||
</div>
|
<a class="nav-link" data-toggle="pill" href="#pills-expense">Expense</a>
|
||||||
<select class="form-control select2" name="provider">
|
</li>
|
||||||
<option>Stripe</option>
|
</ul>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Publishable key <strong class="text-danger">*</strong></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Secret key <strong class="text-danger">*</strong></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Threshold</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00">
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Will not show as an option at Checkout if invoice amount is above this number, 0 disables the threshold check.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="tab-content">
|
||||||
<div class="custom-control custom-switch">
|
|
||||||
<input type="checkbox" class="custom-control-input" name="enable_expense" checked value="1" id="enableExpenseSwitch">
|
|
||||||
<label class="custom-control-label" for="enableExpenseSwitch">Enable Expense</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="tab-pane fade show active" id="pills-details">
|
||||||
<label>Percentage Fee to expense</label>
|
|
||||||
<div class="input-group">
|
<div class="form-group">
|
||||||
<div class="input-group-prepend">
|
<label>Provider <strong class="text-danger">*</strong></label>
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-credit-card"></i></span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control select2" name="provider">
|
||||||
|
<option>Stripe</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" placeholder="Enter Percentage">
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Flat Fee to expense</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-shopping-cart"></i></span>
|
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" placeholder="0.030">
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Secret key <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Income / Expense Account <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control select2" name="account" required>
|
||||||
|
<option value="">- Select an Account -</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sql = mysqli_query($mysqli, "SELECT account_id, account_name FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
|
||||||
|
while ($row = mysqli_fetch_array($sql)) {
|
||||||
|
$account_id = intval($row['account_id']);
|
||||||
|
$account_name = nullable_htmlentities($row['account_name']);
|
||||||
|
?>
|
||||||
|
<option <?php if ($account_name === 'Stripe') { echo "selected"; } ?> value="<?= $account_id ?>"><?= $account_name ?></option>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Threshold</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">Will not show as an option at Checkout if invoice amount is above this number, 0 disables the threshold check.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="pills-expense">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="checkbox" class="custom-control-input" name="enable_expense" checked value="1" id="enableExpenseSwitch">
|
||||||
|
<label class="custom-control-label" for="enableExpenseSwitch">Enable Expense</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Payment Provider Vendor <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control select2" name="expense_vendor" required>
|
||||||
|
<option value="0">Expense Disabled</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
|
||||||
|
while ($row = mysqli_fetch_array($sql)) {
|
||||||
|
$vendor_id = intval($row['vendor_id']);
|
||||||
|
$vendor_name = nullable_htmlentities($row['vendor_name']);
|
||||||
|
?>
|
||||||
|
<option <?php if ($vendor_name === 'Stripe') { echo "selected"; } ?> value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Expense Category <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control select2" name="expense_category" required>
|
||||||
|
<option value="">- Select a Category -</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sql = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Expense' AND category_archived_at IS NULL ORDER BY category_name ASC");
|
||||||
|
while ($row = mysqli_fetch_array($sql)) {
|
||||||
|
$category_id = intval($row['category_id']);
|
||||||
|
$category_name = nullable_htmlentities($row['category_name']);
|
||||||
|
?>
|
||||||
|
<option <?php if ($category_name === 'Processing Fee') { echo "selected"; } ?> value="<?= $category_id ?>"><?= $category_name ?></option>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-secondary ajax-modal" type="button"
|
||||||
|
data-modal-url="../admin/modals/category/category_add.php?category=Expense">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Percentage Fee to expense</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" placeholder="Enter Percentage">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Flat Fee to expense</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" placeholder="0.030">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
|
|||||||
@@ -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 = nullable_htmlentities($row['payment_provider_account']);
|
$account_id = intval($row['payment_provider_account']);
|
||||||
$threshold = floatval($row['payment_provider_threshold']);
|
$threshold = floatval($row['payment_provider_threshold']);
|
||||||
$vendor_id = nullable_htmlentities($row['payment_provider_expense_vendor']);
|
$vendor_id = intval($row['payment_provider_expense_vendor']);
|
||||||
$category_id = nullable_htmlentities($row['payment_provider_expense_category']);
|
$category_id = intval($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,80 +21,170 @@ $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><?php echo $provider_name; ?></strong></h5>
|
<h5 class="modal-title"><i class="fa fa-fw fa-credit-card mr-2"></i>Editing: <strong><?= $provider_name ?></strong></h5>
|
||||||
<button type="button" class="close text-white" data-dismiss="modal">
|
<button type="button" class="close text-white" data-dismiss="modal">
|
||||||
<span>×</span>
|
<span>×</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="<?php echo $_SESSION['csrf_token'] ?>">
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||||
<input type="hidden" name="provider_id" value="<?php echo $provider_id; ?>">
|
<input type="hidden" name="provider_id" value="<?= $provider_id ?>">
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<div class="form-group">
|
<ul class="nav nav-pills nav-justified mb-3">
|
||||||
<label>Publishable key <strong class="text-danger">*</strong></label>
|
<li class="nav-item">
|
||||||
<div class="input-group">
|
<a class="nav-link active" data-toggle="pill" href="#pills-details">Details</a>
|
||||||
<div class="input-group-prepend">
|
</li>
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
|
<li class="nav-item">
|
||||||
</div>
|
<a class="nav-link" data-toggle="pill" href="#pills-expense">Expense</a>
|
||||||
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?php echo $public_key; ?>">
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Secret key <strong class="text-danger">*</strong></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)" value="<?php echo $private_key; ?>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Threshold</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00" value="<?php echo $threshold; ?>">
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Will not show as an option at Checkout if above this number</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="tab-content">
|
||||||
<div class="custom-control custom-switch">
|
|
||||||
<input type="checkbox" class="custom-control-input" name="enable_expense" <?php if ($vendor_id) { echo "checked"; } ?> value="1" id="enableEditExpenseSwitch">
|
|
||||||
<label class="custom-control-label" for="enableEditExpenseSwitch">Enable Expense</label>
|
|
||||||
</div>
|
|
||||||
<small>(Category: Payment Processing -- Vendor: <?php echo $provider_name; ?></small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="tab-pane fade show active" id="pills-details">
|
||||||
<label>Percentage Fee to expense</label>
|
|
||||||
<div class="input-group">
|
<div class="form-group">
|
||||||
<div class="input-group-prepend">
|
<label>Publishable key <strong class="text-danger">*</strong></label>
|
||||||
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" name="public_key" placeholder="Publishable API Key (pk_...)" value="<?= $public_key ?>">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" value="<?php echo $percent_fee; ?>" placeholder="Enter Percentage">
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Flat Fee to expense</label>
|
<label>Secret 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-shopping-cart"></i></span>
|
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" name="private_key" placeholder="Secret API Key (sk_...)" value="<?= $private_key ?>">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" value="<?php echo $flat_fee; ?>" placeholder="0.030">
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Income / Expense Account <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control select2" name="account" required>
|
||||||
|
<option value="">- Select an Account -</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sql = mysqli_query($mysqli, "SELECT account_id, account_name FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
|
||||||
|
while ($row = mysqli_fetch_array($sql)) {
|
||||||
|
$account_id_select = intval($row['account_id']);
|
||||||
|
$account_name = nullable_htmlentities($row['account_name']);
|
||||||
|
?>
|
||||||
|
<option <?php if ($account_id === $account_id_select) { echo "selected"; } ?> value="<?= $account_id_select ?>"><?= $account_name ?></option>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Threshold</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="threshold" placeholder="1000.00" value="<?php echo $threshold; ?>">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">Will not show as an option at Checkout if above this number</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="pills-expense">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Payment Provider Vendor <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control select2" name="expense_vendor" required>
|
||||||
|
<option value="0">Expense Disabled</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
|
||||||
|
while ($row = mysqli_fetch_array($sql)) {
|
||||||
|
$vendor_id_select = intval($row['vendor_id']);
|
||||||
|
$vendor_name = nullable_htmlentities($row['vendor_name']);
|
||||||
|
?>
|
||||||
|
<option <?php if ($vendor_id === $vendor_id_select) { echo "selected"; } ?>
|
||||||
|
value="<?= $vendor_id_select ?>"><?= $vendor_name ?>
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Expense Category <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control select2" name="expense_category" required>
|
||||||
|
<option value="">- Select a Category -</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sql_category = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Expense' AND category_archived_at IS NULL ORDER BY category_name ASC");
|
||||||
|
while ($row = mysqli_fetch_array($sql_category)) {
|
||||||
|
$category_id_select = intval($row['category_id']);
|
||||||
|
$category_name = nullable_htmlentities($row['category_name']);
|
||||||
|
?>
|
||||||
|
<option <?php if ($category_id === $category_id_select) { echo "selected"; } ?> value="<?= $category_id_select ?>"><?= $category_name ?></option>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-secondary ajax-modal" type="button"
|
||||||
|
data-modal-url="../admin/modals/category/category_add.php?category=Expense">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Percentage Fee to expense</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" name="percentage_fee" value="<?php echo $percent_fee; ?>" placeholder="Enter Percentage">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Flat Fee to expense</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,3}" name="flat_fee" value="<?php echo $flat_fee; ?>" placeholder="0.030">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">See <a href="https://stripe.com/pricing" target="_blank">here <i class="fas fa-fw fa-external-link-alt"></i></a> for the latest Stripe Fees.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
<th class="text-center">
|
||||||
<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']);
|
$vendor_name = nullable_htmlentities($row['vendor_name'] ?? "Expense Disabled");
|
||||||
$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,7 +94,9 @@ $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><?php echo $saved_payment_count; ?></td>
|
<td class="text-center">
|
||||||
|
<a class="badge badge-dark badge-pill p-2" href="saved_payment_method.php"><?= $saved_payment_count ?></a>
|
||||||
|
</td>
|
||||||
<td>
|
<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">
|
||||||
|
|||||||
@@ -14,53 +14,20 @@ 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']);
|
||||||
$enable_expense = intval($_POST['enable_expense'] ?? 0);
|
$account = intval($_POST['account']);
|
||||||
|
$expense_vendor = intval($_POST['expense_vendor']) ?? 0;
|
||||||
|
$expense_category = intval($_POST['expense_category']) ?? 0;
|
||||||
$percentage_fee = floatval($_POST['percentage_fee']) / 100 ?? 0;
|
$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 = "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1";
|
$sql = mysqli_query($mysqli, "SELECT 1 FROM payment_providers WHERE payment_provider_name = '$provider' LIMIT 1");
|
||||||
$result = mysqli_query($mysqli, $sql);
|
if (mysqli_num_rows($sql) > 0) {
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Stripe Account, if not create it
|
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");
|
||||||
$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);
|
||||||
|
|
||||||
@@ -81,11 +48,13 @@ 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']);
|
||||||
$enable_expense = intval($_POST['enable_expense'] ?? 0);
|
$account = intval($_POST['account']);
|
||||||
|
$expense_vendor = intval($_POST['expense_vendor']) ?? 0;
|
||||||
|
$expense_category = intval($_POST['expense_category']) ?? 0;
|
||||||
$percentage_fee = floatval($_POST['percentage_fee']) / 100;
|
$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_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee WHERE payment_provider_id = $provider_id");
|
mysqli_query($mysqli,"UPDATE payment_providers SET payment_provider_public_key = '$public_key', payment_provider_private_key = '$private_key', payment_provider_threshold = $threshold, payment_provider_account = $account, payment_provider_expense_vendor = $expense_vendor, payment_provider_expense_category = $expense_category, payment_provider_expense_percentage_fee = $percentage_fee, payment_provider_expense_flat_fee = $flat_fee WHERE payment_provider_id = $provider_id");
|
||||||
|
|
||||||
logAction("Payment Provider", "Edit", "$session_name edited Payment Provider $provider");
|
logAction("Payment Provider", "Edit", "$session_name edited Payment Provider $provider");
|
||||||
|
|
||||||
|
|||||||
@@ -49,3 +49,17 @@ 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<ol class="breadcrumb d-print-none">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="/admin">Admin</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="payment_provider.php">Payment Providers</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active">Saved Payment Methods (Stripe)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
<div class="card card-dark">
|
<div class="card 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>
|
||||||
@@ -107,8 +117,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php echo $client_name; ?> (<?php echo $client_id; ?>)</td>
|
<td>
|
||||||
<td><?php echo $provider_name; ?> (<?php echo $provider_id; ?>)</td>
|
<?= $client_name ?>
|
||||||
|
<br>
|
||||||
|
<small class="text-secondary">ID: <?= $client_id ?></small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= $provider_name ?>
|
||||||
|
<br>
|
||||||
|
<small class="text-secondary">ID: <?= $provider_id ?></small>
|
||||||
|
</td>
|
||||||
<td><?php echo $saved_payment_description; ?></td>
|
<td><?php echo $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>
|
||||||
|
|||||||
@@ -57,11 +57,12 @@ 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";
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ if (isset($_GET['share_generate_link'])) {
|
|||||||
$item_expires_friendly = "1 month";
|
$item_expires_friendly = "1 month";
|
||||||
}
|
}
|
||||||
|
|
||||||
$item_key = randomString(156);
|
$item_key = randomString(32);
|
||||||
|
|
||||||
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,3 +992,23 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
<?php require_once "includes/inc_all.php"; ?>
|
|
||||||
|
|
||||||
<!-- Breadcrumbs-->
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="index.html">Dashboard</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item active">Blank Page</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<!-- Page Content -->
|
|
||||||
<h1>Blank Page</h1>
|
|
||||||
<hr>
|
|
||||||
<p>This is a great starting point for new custom pages.</p>
|
|
||||||
<h1><?php echo $session_user_role; ?></h1>
|
|
||||||
<?php validateAdminRole(); ?>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$start_date = date('Y') . "-10-10";
|
|
||||||
|
|
||||||
echo "<H1>$start_date</H1>";
|
|
||||||
|
|
||||||
echo "<H2>User Agent</H2>";
|
|
||||||
echo getUserAgent();
|
|
||||||
|
|
||||||
|
|
||||||
?>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<input type="tel" name="phone" id="phone">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Minimal</label>
|
|
||||||
<select class="form-control select2 select2-hidden-accessible" style="width: 100%;" data-select2-id="1" tabindex="-1" aria-hidden="true">
|
|
||||||
<option selected="selected" data-select2-id="3">Alabama</option>
|
|
||||||
<option data-select2-id="35">Alaska</option>
|
|
||||||
<option data-select2-id="36">California</option>
|
|
||||||
<option data-select2-id="37">Delaware</option>
|
|
||||||
<option data-select2-id="38">Tennessee</option>
|
|
||||||
<option data-select2-id="39">Texas</option>
|
|
||||||
<option data-select2-id="40">Washington</option>
|
|
||||||
</select><span class="select2 select2-container select2-container--default select2-container--below" dir="ltr" data-select2-id="2" style="width: 100%;"><span class="selection"><span class="select2-selection select2-selection--single" role="combobox" aria-haspopup="true" aria-expanded="false" tabindex="0" aria-disabled="false" aria-labelledby="select2-nbex-container"><span class="select2-selection__rendered" id="select2-nbex-container" role="textbox" aria-readonly="true" title="Alabama">Alabama</span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span></span></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt>Requester</dt>
|
|
||||||
<dd>Sam Adams</dd>
|
|
||||||
|
|
||||||
<dt>Created</dt>
|
|
||||||
<dd><time datetime="2024-04-11T17:52:30+00:00" title="2024-04-11 13:52" data-datetime="calendar">Today at 13:52</time></dd>
|
|
||||||
|
|
||||||
<dt>Last activity</dt>
|
|
||||||
<dd><time datetime="2024-04-11T18:08:55+00:00" title="2024-04-11 14:08" data-datetime="calendar">Today at 14:08</time></dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<?php echo randomString(100); ?>
|
|
||||||
<br>
|
|
||||||
<textarea class="tinymceTest"></textarea>
|
|
||||||
|
|
||||||
<textarea class="tinymce"></textarea>
|
|
||||||
|
|
||||||
<textarea class="tinymceTicket"></textarea>
|
|
||||||
<?php
|
|
||||||
// show the current Date and Time
|
|
||||||
$date_time = date('Y-m-d H:i:s');
|
|
||||||
echo "Current Date and Time: <strong>$date_time</strong>";
|
|
||||||
?>
|
|
||||||
|
|
||||||
<script>toastr.success('Have Fun Wozz!!')</script>
|
|
||||||
|
|
||||||
<?php require_once "../includes/footer.php";
|
|
||||||
|
|||||||
@@ -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-sm">
|
<div class="table-responsive">
|
||||||
<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>
|
||||||
|
|||||||
@@ -797,7 +797,9 @@ 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" data-toggle="modal" data-target="#addTicketModal">
|
<button type="button" class="btn btn-primary ajax-modal"
|
||||||
|
data-modal-url="modals/ticket/ticket_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>"
|
||||||
|
data-modal-size="lg">
|
||||||
<i class="fas fa-plus mr-2"></i>New Ticket
|
<i class="fas fa-plus mr-2"></i>New Ticket
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -152,8 +152,6 @@ ob_start();
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($client_id) { ?>
|
<?php if ($client_id) { ?>
|
||||||
|
|||||||
@@ -463,32 +463,25 @@ ob_start();
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Ticket Templates -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
$(document).on('change', '#ticket_template_select', function () {
|
||||||
var templateSelect = $('#ticket_template_select');
|
const $opt = $(this).find(':selected');
|
||||||
var subjectInput = document.getElementById('subjectInput');
|
const templateSubject = $opt.data('subject') || '';
|
||||||
var detailsInput = document.getElementById('detailsInput');
|
const templateDetails = $opt.data('details') || '';
|
||||||
|
|
||||||
templateSelect.on('select2:select', function(e) {
|
$('#subjectInput').val(templateSubject);
|
||||||
var selectedOption = e.params.data.element;
|
|
||||||
var templateSubject = selectedOption.getAttribute('data-subject');
|
|
||||||
var templateDetails = selectedOption.getAttribute('data-details');
|
|
||||||
|
|
||||||
// Update Subject
|
if (window.tinymce) {
|
||||||
subjectInput.value = templateSubject || '';
|
const editor = tinymce.get('detailsInput');
|
||||||
|
if (editor) {
|
||||||
// Update Details
|
editor.setContent(templateDetails);
|
||||||
if (typeof tinymce !== 'undefined') {
|
|
||||||
var editor = tinymce.get('detailsInput');
|
|
||||||
if (editor) {
|
|
||||||
editor.setContent(templateDetails || '');
|
|
||||||
} else {
|
|
||||||
detailsInput.value = templateDetails || '';
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
detailsInput.value = templateDetails || '';
|
$('#detailsInput').val(templateDetails);
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
$('#detailsInput').val(templateDetails);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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,32 +297,24 @@ ob_start();
|
|||||||
|
|
||||||
<!-- Ticket Templates -->
|
<!-- Ticket Templates -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
$(document).on('change', '#ticket_template_select', function () {
|
||||||
var templateSelect = $('#ticket_template_select');
|
const $opt = $(this).find(':selected');
|
||||||
var subjectInput = document.getElementById('subjectInput');
|
const templateSubject = $opt.data('subject') || '';
|
||||||
var detailsInput = document.getElementById('detailsInput');
|
const templateDetails = $opt.data('details') || '';
|
||||||
|
|
||||||
templateSelect.on('select2:select', function(e) {
|
$('#subjectInput').val(templateSubject);
|
||||||
var selectedOption = e.params.data.element;
|
|
||||||
var templateSubject = selectedOption.getAttribute('data-subject');
|
|
||||||
var templateDetails = selectedOption.getAttribute('data-details');
|
|
||||||
|
|
||||||
// Update Subject
|
if (window.tinymce) {
|
||||||
subjectInput.value = templateSubject || '';
|
const editor = tinymce.get('detailsInput');
|
||||||
|
if (editor) {
|
||||||
// Update Details
|
editor.setContent(templateDetails);
|
||||||
if (typeof tinymce !== 'undefined') {
|
} else {
|
||||||
var editor = tinymce.get('detailsInput');
|
$('#detailsInput').val(templateDetails);
|
||||||
if (editor) {
|
}
|
||||||
editor.setContent(templateDetails || '');
|
} else {
|
||||||
} else {
|
$('#detailsInput').val(templateDetails);
|
||||||
detailsInput.value = templateDetails || '';
|
}
|
||||||
}
|
});
|
||||||
} else {
|
|
||||||
detailsInput.value = templateDetails || '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Ticket Client/Contact JS -->
|
<!-- Ticket Client/Contact JS -->
|
||||||
|
|||||||
140
agent/modals/ticket/ticket_task_approver_add.php
Normal file
140
agent/modals/ticket/ticket_task_approver_add.php
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once '../../../includes/modal_header.php';
|
||||||
|
|
||||||
|
$task_id = intval($_GET['id']);
|
||||||
|
|
||||||
|
$sql = mysqli_query($mysqli, "SELECT * FROM tasks
|
||||||
|
WHERE task_id = $task_id
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
|
||||||
|
$row = mysqli_fetch_array($sql);
|
||||||
|
$task_name = nullable_htmlentities($row['task_name']);
|
||||||
|
|
||||||
|
// Generate the HTML form content using output buffering.
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="modal-header bg-dark">
|
||||||
|
<h5 class="modal-title"><i class="fa fa-fw fa-shield-alt mr-2"></i>New approver for task <?=$task_name?></h5>
|
||||||
|
<button type="button" class="close text-white" data-dismiss="modal">
|
||||||
|
<span>×</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';
|
||||||
@@ -14,6 +14,14 @@ $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();
|
||||||
|
|
||||||
@@ -50,6 +58,52 @@ 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">
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_category = $category_id, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_status = $ticket_status, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_url_key = '$url_key', ticket_client_id = $client_id, ticket_project_id = $project_id");
|
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");
|
||||||
|
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $invoice_number, invoice_scope = '$scope', invoice_date = '$date', invoice_due = DATE_ADD('$date', INTERVAL $client_net_terms day), invoice_discount_amount = '$invoice_discount', invoice_amount = '$invoice_amount', invoice_currency_code = '$session_company_currency', invoice_category_id = $category, invoice_status = 'Draft', invoice_url_key = '$url_key', invoice_client_id = $client_id");
|
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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$invoice_scope', invoice_date = '$date', invoice_due = DATE_ADD('$date', INTERVAL $client_net_terms day), invoice_category_id = $category_id, invoice_status = 'Draft', invoice_discount_amount = $invoice_discount_amount, invoice_amount = $invoice_amount, invoice_currency_code = '$invoice_currency_code', invoice_note = '$invoice_note', invoice_url_key = '$url_key', invoice_client_id = $client_id");
|
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$invoice_scope', invoice_date = '$date', invoice_due = DATE_ADD('$date', INTERVAL $client_net_terms day), invoice_category_id = $category_id, invoice_status = 'Draft', invoice_discount_amount = $invoice_discount_amount, invoice_amount = $invoice_amount, invoice_currency_code = '$invoice_currency_code', invoice_note = '$invoice_note', invoice_url_key = '$url_key', invoice_client_id = $client_id");
|
||||||
|
|
||||||
@@ -570,15 +570,13 @@ if (isset($_GET['email_invoice'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Queue Mail
|
// 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);
|
||||||
@@ -613,15 +611,13 @@ 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);
|
||||||
@@ -659,7 +655,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 $client_query ORDER BY invoice_number ASC");
|
$sql = mysqli_query($mysqli,"SELECT * FROM invoices LEFT JOIN clients ON invoice_client_id = client_id WHERE $date_query AND $client_query ORDER BY invoice_number ASC");
|
||||||
|
|
||||||
$num_rows = mysqli_num_rows($sql);
|
$num_rows = mysqli_num_rows($sql);
|
||||||
|
|
||||||
|
|||||||
@@ -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(156);
|
$quote_url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO quotes SET quote_prefix = '$config_quote_prefix', quote_number = $quote_number, quote_scope = '$scope', quote_date = '$date', quote_expire = '$expire', quote_currency_code = '$session_company_currency', quote_category_id = $category, quote_status = 'Draft', quote_url_key = '$quote_url_key', quote_client_id = $client_id");
|
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(156);
|
$quote_url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO quotes SET quote_prefix = '$config_quote_prefix', quote_number = $quote_number, quote_scope = '$quote_scope', quote_date = '$date', quote_expire = '$expire', quote_category_id = $category_id, quote_status = 'Draft', quote_discount_amount = $quote_discount_amount, quote_amount = $quote_amount, quote_currency_code = '$quote_currency_code', quote_note = '$quote_note', quote_url_key = '$quote_url_key', quote_client_id = $client_id");
|
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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $invoice_number, invoice_scope = '$quote_scope', invoice_date = '$date', invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_category_id = $category_id, invoice_status = 'Draft', invoice_discount_amount = $quote_discount_amount, invoice_amount = $quote_amount, invoice_currency_code = '$quote_currency_code', invoice_note = '$quote_note', invoice_url_key = '$url_key', invoice_client_id = $client_id");
|
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");
|
||||||
|
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli,"INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Sent', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id");
|
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");
|
||||||
|
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
$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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
$ticket_status = 1; // Default
|
$ticket_status = 1; // Default
|
||||||
if ($assigned_id > 0) {
|
if ($assigned_id > 0) {
|
||||||
|
|||||||
@@ -155,6 +155,247 @@ if (isset($_GET['undo_complete_task'])) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['add_ticket_task_approver'])) {
|
||||||
|
|
||||||
|
validateCSRFToken($_POST['csrf_token']);
|
||||||
|
enforceUserPermission('module_support', 2);
|
||||||
|
|
||||||
|
$task_id = intval($_POST['task_id']);
|
||||||
|
$scope = sanitizeInput($_POST['approval_scope']);
|
||||||
|
$type = sanitizeInput($_POST['approval_type']);
|
||||||
|
$approval_url_key = randomString(32);
|
||||||
|
|
||||||
|
$required_user_id = "NULL";
|
||||||
|
if ($type == 'specific') {
|
||||||
|
$required_user_id = intval($_POST['approval_required_user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
mysqli_query($mysqli, "INSERT INTO task_approvals SET approval_scope = '$scope', approval_type = '$type', approval_required_user_id = $required_user_id, approval_status = 'pending', approval_created_by = $session_user_id, approval_url_key = '$approval_url_key', approval_task_id = $task_id");
|
||||||
|
|
||||||
|
$approval_id = mysqli_insert_id($mysqli);
|
||||||
|
|
||||||
|
// Task/Ticket Info
|
||||||
|
$tt_row = mysqli_fetch_array(mysqli_query($mysqli, "
|
||||||
|
SELECT * FROM tasks
|
||||||
|
LEFT JOIN tickets ON ticket_id = task_ticket_id
|
||||||
|
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
|
||||||
|
WHERE task_id = $task_id LIMIT 1
|
||||||
|
")
|
||||||
|
);
|
||||||
|
$task_name = sanitizeInput($tt_row['task_name']);
|
||||||
|
$ticket_id = intval($tt_row['task_ticket_id']);
|
||||||
|
$ticket_prefix = sanitizeInput($tt_row['ticket_prefix']);
|
||||||
|
$ticket_number = intval($tt_row['ticket_number']);
|
||||||
|
$ticket_subject = sanitizeInput($tt_row['ticket_subject']);
|
||||||
|
$ticket_status = sanitizeInput($tt_row['ticket_status_name']);
|
||||||
|
$ticket_url_key = sanitizeInput($tt_row['ticket_url_key']);
|
||||||
|
$ticket_contact_id = intval($tt_row['ticket_contact_id']);
|
||||||
|
$client_id = intval($tt_row['ticket_client_id']);
|
||||||
|
|
||||||
|
// --Notifications--
|
||||||
|
|
||||||
|
// Sanitize Config vars from get_settings.php
|
||||||
|
$config_ticket_from_name = sanitizeInput($config_ticket_from_name);
|
||||||
|
$config_ticket_from_email = sanitizeInput($config_ticket_from_email);
|
||||||
|
$config_base_url = sanitizeInput($config_base_url);
|
||||||
|
|
||||||
|
// Get Company Info
|
||||||
|
$crow = mysqli_fetch_array(mysqli_query($mysqli, "SELECT company_name, company_phone, company_phone_country_code FROM companies WHERE company_id = 1"));
|
||||||
|
$company_name = sanitizeInput($crow['company_name']);
|
||||||
|
$company_phone = sanitizeInput(formatPhoneNumber($crow['company_phone'], $crow['company_phone_country_code']));
|
||||||
|
|
||||||
|
// Email contents
|
||||||
|
$subject = "Ticket task approval required - [$ticket_prefix$ticket_number] - $ticket_subject";
|
||||||
|
$body = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello,<br><br>A ticket regarding $ticket_subject has a task requiring your approval:- <br>Task name: $task_name<br>Scope/Type: $scope - $type <br><br>To approve this task, please click <a href=\'https://$config_base_url/guest/guest_approve_ticket_task.php?task_approval_id=$approval_id&url_key=$approval_url_key\'>here</a>.<br>If you require further information, please reply to this e-mail.<br><br>Ticket: $ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Status: $ticket_status<br>Portal: <a href=\'https://$config_base_url/guest/guest_view_ticket.php?ticket_id=$ticket_id&url_key=$ticket_url_key\'>View ticket</a><br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
||||||
|
|
||||||
|
if ($scope == 'internal' && $type == 'specific' && $session_user_id !== $required_user_id) {
|
||||||
|
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$session_name needs your approval for ticket $ticket_prefix$ticket_number task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_client_id = 0, notification_user_id = $required_user_id");
|
||||||
|
|
||||||
|
if (!empty($config_smtp_host)) {
|
||||||
|
$agent_contact = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_name, user_email FROM users WHERE user_id = $required_user_id AND user_archived_at IS NULL"));
|
||||||
|
$name = sanitizeInput($agent_contact['user_name']);
|
||||||
|
$email = sanitizeInput($agent_contact['user_email']);
|
||||||
|
|
||||||
|
// Only add contact to email queue if email is valid
|
||||||
|
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$data[] = [
|
||||||
|
'from' => $config_ticket_from_email,
|
||||||
|
'from_name' => $config_ticket_from_name,
|
||||||
|
'recipient' => $email,
|
||||||
|
'recipient_name' => $name,
|
||||||
|
'subject' => $subject,
|
||||||
|
'body' => $body
|
||||||
|
];
|
||||||
|
|
||||||
|
addToMailQueue($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($config_smtp_host) && $scope == 'client' && $type == 'any') {
|
||||||
|
|
||||||
|
$contact_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name, contact_email FROM contacts WHERE contact_id = $ticket_contact_id LIMIT 1"));
|
||||||
|
$contact_name = sanitizeInput($contact_row['contact_name']);
|
||||||
|
$contact_email = sanitizeInput($contact_row['contact_email']);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
if (filter_var($contact_email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$data[] = [
|
||||||
|
'from' => $config_ticket_from_email,
|
||||||
|
'from_name' => $config_ticket_from_name,
|
||||||
|
'recipient' => $contact_email,
|
||||||
|
'recipient_name' => $contact_name,
|
||||||
|
'subject' => $subject,
|
||||||
|
'body' => $body
|
||||||
|
];
|
||||||
|
addToMailQueue($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($config_smtp_host) && $scope == 'client' && $type == 'technical') {
|
||||||
|
|
||||||
|
$sql_technical_contacts = mysqli_query(
|
||||||
|
$mysqli,
|
||||||
|
"SELECT contact_name, contact_email FROM contacts
|
||||||
|
WHERE contact_technical = 1
|
||||||
|
AND contact_email != ''
|
||||||
|
AND contact_client_id = $client_id"
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
while ($technical_contact = mysqli_fetch_array($sql_technical_contacts)) {
|
||||||
|
$technical_contact_name = sanitizeInput($technical_contact['contact_name']);
|
||||||
|
$technical_contact_email = sanitizeInput($technical_contact['contact_email']);
|
||||||
|
|
||||||
|
if (filter_var($technical_contact_email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$data[] = [
|
||||||
|
'from' => $config_ticket_from_email,
|
||||||
|
'from_name' => $config_ticket_from_name,
|
||||||
|
'recipient' => $technical_contact_email,
|
||||||
|
'recipient_name' => $technical_contact_name,
|
||||||
|
'subject' => $subject,
|
||||||
|
'body' => $body
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
addToMailQueue($data);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($config_smtp_host) && $scope == 'client' && $type == 'billing') {
|
||||||
|
|
||||||
|
$sql_billing_contacts = mysqli_query(
|
||||||
|
$mysqli,
|
||||||
|
"SELECT contact_name, contact_email FROM contacts
|
||||||
|
WHERE contact_billing = 1
|
||||||
|
AND contact_email != ''
|
||||||
|
AND contact_client_id = $client_id"
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
while ($billing_contact = mysqli_fetch_array($sql_billing_contacts)) {
|
||||||
|
$billing_contact_name = sanitizeInput($billing_contact['contact_name']);
|
||||||
|
$billing_contact_email = sanitizeInput($billing_contact['contact_email']);
|
||||||
|
|
||||||
|
if (filter_var($billing_contact_email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$data[] = [
|
||||||
|
'from' => $config_ticket_from_email,
|
||||||
|
'from_name' => $config_ticket_from_name,
|
||||||
|
'recipient' => $billing_contact_email,
|
||||||
|
'recipient_name' => $billing_contact_name,
|
||||||
|
'subject' => $subject,
|
||||||
|
'body' => $body
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
addToMailQueue($data);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
logAction("Task", "Edit", "$session_name added task approver for $task_name", $client_id, $task_id);
|
||||||
|
|
||||||
|
flash_alert("Added approver");
|
||||||
|
redirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['approve_ticket_task'])) {
|
||||||
|
|
||||||
|
validateCSRFToken($_GET['csrf_token']);
|
||||||
|
enforceUserPermission('module_support', 2);
|
||||||
|
|
||||||
|
$task_id = intval($_GET['approve_task']);
|
||||||
|
$approval_id = intval($_GET['approval_id']);
|
||||||
|
|
||||||
|
$approval_row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_scope = 'internal'"));
|
||||||
|
|
||||||
|
$task_name = nullable_htmlentities($approval_row['task_name']);
|
||||||
|
$scope = nullable_htmlentities($approval_row['approval_scope']);
|
||||||
|
$type = nullable_htmlentities($approval_row['approval_type']);
|
||||||
|
$required_user = intval($approval_row['approval_required_user_id']);
|
||||||
|
$created_by = intval($approval_row['approval_created_by']);
|
||||||
|
$ticket_id = intval($approval_row['task_ticket_id']);
|
||||||
|
|
||||||
|
if (!$approval_row) {
|
||||||
|
flash_alert("Cannot find/approve that task", 'error');
|
||||||
|
redirect();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate approver (deny)
|
||||||
|
if ($required_user > 0 && $required_user !== $session_user_id) {
|
||||||
|
flash_alert("You cannot approve that task", 'error');
|
||||||
|
redirect();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($required_user == 0 && $type == 'any' && $created_by == $session_user_id) {
|
||||||
|
flash_alert("You cannot approve your own task", 'error');
|
||||||
|
redirect();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approve
|
||||||
|
mysqli_query($mysqli, "UPDATE task_approvals SET approval_status = 'approved', approval_approved_by = $session_user_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_scope = 'internal'");
|
||||||
|
|
||||||
|
// Notify
|
||||||
|
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$session_name approved ticket task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_client_id = 0, notification_user_id = $created_by");
|
||||||
|
// TODO: Email agent
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
logAction("Task", "Edit", "$session_name approved task $task_name (approval $approval_id)", 0, $task_id);
|
||||||
|
|
||||||
|
flash_alert("Approved");
|
||||||
|
redirect();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['delete_ticket_task_approver'])) {
|
||||||
|
|
||||||
|
validateCSRFToken($_GET['csrf_token']);
|
||||||
|
|
||||||
|
enforceUserPermission('module_support', 3);
|
||||||
|
|
||||||
|
$approval_id = intval($_GET['delete_ticket_task_approver']);
|
||||||
|
|
||||||
|
mysqli_query($mysqli, "DELETE FROM task_approvals WHERE approval_id = $approval_id");
|
||||||
|
|
||||||
|
logAction("Task", "Delete", "$session_name deleted task approval request ($approval_id)", 0, 0);
|
||||||
|
|
||||||
|
flash_alert("Approval request deleted", 'error');
|
||||||
|
|
||||||
|
redirect();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($_GET['complete_all_tasks'])) {
|
if (isset($_GET['complete_all_tasks'])) {
|
||||||
|
|
||||||
enforceUserPermission('module_support', 2);
|
enforceUserPermission('module_support', 2);
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Agent', ticket_category = $category_id, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = '$billable', ticket_status = '$ticket_status', ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_location_id = $location_id, ticket_asset_id = $asset_id, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_url_key = '$url_key', ticket_due_at = $due, ticket_client_id = $client_id, ticket_invoice_id = 0, ticket_project_id = $project_id");
|
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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_category = $category_id, ticket_subject = '$subject_asset_prepended', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_status = $ticket_status, ticket_asset_id = $asset_id, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_url_key = '$url_key', ticket_client_id = $client_id, ticket_project_id = $project_id");
|
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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli, "INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $invoice_number, invoice_scope = '$scope', invoice_date = '$date', invoice_due = DATE_ADD('$date', INTERVAL $client_net_terms day), invoice_currency_code = '$session_company_currency', invoice_category_id = $category, invoice_status = 'Draft', invoice_url_key = '$url_key', invoice_client_id = $client_id");
|
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);
|
||||||
|
|||||||
@@ -961,23 +961,82 @@ 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) { ?>
|
||||||
<a href="post.php?complete_task=<?php echo $task_id; ?>">
|
|
||||||
<i class="far fa-square text-dark"></i>
|
<?php if ($task_needs_approval) { ?>
|
||||||
</a>
|
<i class="fas fa-shield-alt text-warning"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="top"
|
||||||
|
title="Approval required"></i>
|
||||||
|
|
||||||
|
<?php if ($user_can_approve) { ?>
|
||||||
|
<a class="confirm-link" href="post.php?approve_ticket_task=<?= $task_id ?>&approval_id=<?= $approval_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||||
|
<i class="fas fa-thumbs-up text-green"></i>
|
||||||
|
</a>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
<span class="text-dark ml-2"><?= $task_name ?></span>
|
||||||
|
|
||||||
|
<?php } else { ?>
|
||||||
|
<a href="post.php?complete_task=<?php echo $task_id; ?>">
|
||||||
|
<i class="far fa-square text-dark"></i>
|
||||||
|
</a>
|
||||||
|
<span class="text-dark ml-2"><?php echo $task_name; ?></span>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
<span class="text-dark ml-2"><?php echo $task_name; ?></span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
@@ -997,6 +1056,12 @@ if (isset($_GET['ticket_id'])) {
|
|||||||
data-modal-url="modals/ticket/ticket_task_edit.php?id=<?= $task_id ?>">
|
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
|
||||||
|
|||||||
@@ -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-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=ticket_billable&order=<?php echo $disp; ?>">
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ticket_billable&order=<?= $disp ?>">
|
||||||
Billable <?php if ($sort == 'ticket_billable') { echo $order_icon; } ?>
|
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'>Yes</span>";
|
echo "<span class='badge badge-pill badge-success p-2'><i class='fas fa-fw fa-check'></i></span>";
|
||||||
} else {
|
} else {
|
||||||
echo "<span class='badge badge-pill badge-secondary p-2'>No</span>";
|
echo "<span class='badge badge-pill badge-secondary p-2'><i class='fas fa-fw fa-minus'></i></span>";
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -51,4 +51,3 @@ $data = "otpauth://totp/ITFlow:$session_email?secret=$token";
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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(156);
|
$extension_key = randomString(32);
|
||||||
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";
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
$insert_sql = mysqli_query($mysqli,"INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'API', ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $billable, ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_created_by = 0, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_asset_id = $asset, ticket_url_key = '$url_key', ticket_client_id = $client_id");
|
$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
|
||||||
|
|||||||
@@ -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: /client/login.php");
|
header("Location: /login.php");
|
||||||
die;
|
die;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check user type
|
// Check user type
|
||||||
if ($_SESSION['user_type'] !== 2) {
|
if ($_SESSION['user_type'] !== 2) {
|
||||||
header("Location: /client/login.php");
|
header("Location: /login.php");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
234
client/login.php
234
client/login.php
@@ -1,234 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* Client Portal
|
|
||||||
* Landing / Home page for the client portal
|
|
||||||
*/
|
|
||||||
|
|
||||||
header("Content-Security-Policy: default-src 'self'");
|
|
||||||
|
|
||||||
require_once '../config.php';
|
|
||||||
|
|
||||||
require_once '../functions.php';
|
|
||||||
|
|
||||||
require_once '../includes/load_global_settings.php';
|
|
||||||
|
|
||||||
if (!isset($_SESSION)) {
|
|
||||||
// HTTP Only cookies
|
|
||||||
ini_set("session.cookie_httponly", true);
|
|
||||||
if ($config_https_only) {
|
|
||||||
// Tell client to only send cookie(s) over HTTPS
|
|
||||||
ini_set("session.cookie_secure", true);
|
|
||||||
}
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Timezone after session_start
|
|
||||||
require_once "../includes/inc_set_timezone.php";
|
|
||||||
|
|
||||||
// Check to see if client portal is enabled
|
|
||||||
if($config_client_portal_enable == 0) {
|
|
||||||
echo "Client Portal is Disabled";
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$session_ip = sanitizeInput(getIP());
|
|
||||||
$session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT']);
|
|
||||||
|
|
||||||
$sql_settings = mysqli_query($mysqli, "SELECT config_azure_client_id, config_login_message FROM settings WHERE company_id = 1");
|
|
||||||
$settings = mysqli_fetch_array($sql_settings);
|
|
||||||
$azure_client_id = $settings['config_azure_client_id'];
|
|
||||||
$config_login_message = nullable_htmlentities($settings['config_login_message']);
|
|
||||||
|
|
||||||
$company_sql = mysqli_query($mysqli, "SELECT company_name, company_logo FROM companies WHERE company_id = 1");
|
|
||||||
$company_results = mysqli_fetch_array($company_sql);
|
|
||||||
$company_name = $company_results['company_name'];
|
|
||||||
$company_logo = $company_results['company_logo'];
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['login'])) {
|
|
||||||
|
|
||||||
$email = sanitizeInput($_POST['email']);
|
|
||||||
$password = $_POST['password'];
|
|
||||||
|
|
||||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
||||||
|
|
||||||
header("HTTP/1.1 401 Unauthorized");
|
|
||||||
|
|
||||||
$_SESSION['login_message'] = 'Invalid e-mail';
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
$sql = mysqli_query($mysqli, "SELECT * FROM users
|
|
||||||
LEFT JOIN contacts ON user_id = contact_user_id
|
|
||||||
LEFT JOIN clients ON contact_client_id = client_id
|
|
||||||
WHERE user_email = '$email'
|
|
||||||
AND client_archived_at IS NULL
|
|
||||||
AND user_archived_at IS NULL
|
|
||||||
AND user_type = 2
|
|
||||||
AND user_status = 1
|
|
||||||
LIMIT 1"
|
|
||||||
);
|
|
||||||
|
|
||||||
$row = mysqli_fetch_array($sql);
|
|
||||||
$client_id = intval($row['contact_client_id']);
|
|
||||||
$user_id = intval($row['user_id']);
|
|
||||||
$session_user_id = $user_id; // to pass the user_id to logAction function
|
|
||||||
$contact_id = intval($row['contact_id']);
|
|
||||||
$user_email = sanitizeInput($row['user_email']);
|
|
||||||
$user_auth_method = sanitizeInput($row['user_auth_method']);
|
|
||||||
|
|
||||||
if ($user_auth_method == 'local') {
|
|
||||||
if (password_verify($password, $row['user_password'])) {
|
|
||||||
|
|
||||||
$_SESSION['client_logged_in'] = true;
|
|
||||||
$_SESSION['client_id'] = $client_id;
|
|
||||||
$_SESSION['user_id'] = $user_id;
|
|
||||||
$_SESSION['user_type'] = 2;
|
|
||||||
$_SESSION['contact_id'] = $contact_id;
|
|
||||||
$_SESSION['login_method'] = "local";
|
|
||||||
|
|
||||||
header("Location: index.php");
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAction("Client Login", "Success", "Client contact $user_email successfully logged in locally", $client_id, $user_id);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (incorrect password for contact ID $contact_id)", $client_id, $user_id);
|
|
||||||
|
|
||||||
header("HTTP/1.1 401 Unauthorized");
|
|
||||||
$_SESSION['login_message'] = 'Incorrect username or password.';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid email/not allowed local auth)");
|
|
||||||
|
|
||||||
header("HTTP/1.1 401 Unauthorized");
|
|
||||||
|
|
||||||
$_SESSION['login_message'] = 'Incorrect username or password.';
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<title><?php echo $company_name; ?> | Client Portal Login</title>
|
|
||||||
|
|
||||||
<!-- Tell the browser to be responsive to screen width -->
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="robots" content="noindex">
|
|
||||||
|
|
||||||
<!-- Favicon - If Fav Icon exists else use the default one -->
|
|
||||||
<?php if(file_exists('../uploads/favicon.ico')) { ?>
|
|
||||||
<link rel="icon" type="image/x-icon" href="../uploads/favicon.ico">
|
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
|
||||||
<link rel="stylesheet" href="../plugins/fontawesome-free/css/all.min.css">
|
|
||||||
|
|
||||||
<!-- Theme style -->
|
|
||||||
<link rel="stylesheet" href="../plugins/adminlte/css/adminlte.min.css">
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="hold-transition login-page">
|
|
||||||
<div class="login-box">
|
|
||||||
<div class="login-logo">
|
|
||||||
<?php if (!empty($company_logo)) { ?>
|
|
||||||
<img alt="<?=$company_name?> logo" height="110" width="380" class="img-fluid" src="<?php echo "../uploads/settings/$company_logo"; ?>">
|
|
||||||
<?php } else { ?>
|
|
||||||
<b><?=$company_name?></b> <br>Client Portal Login</h2>
|
|
||||||
<?php } ?>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body login-card-body">
|
|
||||||
<?php if(!empty($config_login_message)){ ?>
|
|
||||||
<p class="login-box-msg px-0"><?php echo nl2br($config_login_message); ?></p>
|
|
||||||
<?php } ?>
|
|
||||||
<?php
|
|
||||||
if (!empty($_SESSION['login_message'])) { ?>
|
|
||||||
<p class="login-box-msg text-danger">
|
|
||||||
<?php
|
|
||||||
echo $_SESSION['login_message'];
|
|
||||||
unset($_SESSION['login_message']);
|
|
||||||
?>
|
|
||||||
</p>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<form method="post">
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input type="text" class="form-control" placeholder="Registered Client Email" name="email" required autofocus>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<span class="fas fa-envelope"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input type="password" class="form-control" placeholder="Client Password" name="password" required>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<span class="fas fa-lock"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-success btn-block mb-3" name="login">Sign in</button>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
if (!empty($config_smtp_host)) { ?>
|
|
||||||
<h5 class="text-center"><a href="login_reset.php">Forgot password?</a></h5>
|
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
if (!empty($azure_client_id)) { ?>
|
|
||||||
<hr>
|
|
||||||
<div class="col text-center">
|
|
||||||
<a href="login_microsoft.php">
|
|
||||||
<button type="button" class="btn btn-secondary">Login with Microsoft Entra</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- /.login-card-body -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- /.div.card -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- /.login-box -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
if (!$config_whitelabel_enabled) {
|
|
||||||
echo '<small class="text-muted">Powered by ITFlow</small>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- jQuery -->
|
|
||||||
<script src="../plugins/jquery/jquery.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Bootstrap 4 -->
|
|
||||||
<script src="../plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!-- AdminLTE App -->
|
|
||||||
<script src="../plugins/adminlte/js/adminlte.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Prevents resubmit on refresh or back -->
|
|
||||||
<script src="../js/login_prevent_resubmit.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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(156);
|
$token = randomString(32);
|
||||||
$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>
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
// 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,6 +185,43 @@ if (isset($_POST['add_ticket_comment'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['approve_ticket_task'])) {
|
||||||
|
|
||||||
|
$task_id = intval($_GET['approve_ticket_task']);
|
||||||
|
$approval_id = intval($_GET['approval_id']);
|
||||||
|
$url_key = sanitizeInput($_GET['approval_url_key']);
|
||||||
|
|
||||||
|
$approval_row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending' AND approval_scope = 'client'"));
|
||||||
|
|
||||||
|
$task_name = nullable_htmlentities($approval_row['task_name']);
|
||||||
|
$scope = nullable_htmlentities($approval_row['approval_scope']);
|
||||||
|
$type = nullable_htmlentities($approval_row['approval_type']);
|
||||||
|
$required_user = intval($approval_row['approval_required_user_id']);
|
||||||
|
$created_by = intval($approval_row['approval_created_by']);
|
||||||
|
$ticket_id = intval($approval_row['task_ticket_id']);
|
||||||
|
|
||||||
|
if (!$approval_row) {
|
||||||
|
flash_alert("Cannot find/approve that task", 'warning');
|
||||||
|
redirect();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approve
|
||||||
|
mysqli_query($mysqli, "UPDATE task_approvals SET approval_status = 'approved', approval_approved_by = $session_user_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending' AND approval_scope = 'client'");
|
||||||
|
|
||||||
|
|
||||||
|
// Notify tech
|
||||||
|
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$session_contact_email approved ticket task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_client_id = $session_client_id, notification_user_id = $created_by");
|
||||||
|
// TODO: Email agent
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
logAction("Task", "Edit", "Contact $session_contact_email approved task $task_name (approval $approval_id)", $session_client_id, $task_id);
|
||||||
|
|
||||||
|
flash_alert("Task Approved");
|
||||||
|
redirect();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($_POST['add_ticket_feedback'])) {
|
if (isset($_POST['add_ticket_feedback'])) {
|
||||||
|
|
||||||
$ticket_id = intval($_POST['ticket_id']);
|
$ticket_id = intval($_POST['ticket_id']);
|
||||||
@@ -320,7 +357,7 @@ if (isset($_GET['logout'])) {
|
|||||||
session_unset();
|
session_unset();
|
||||||
session_destroy();
|
session_destroy();
|
||||||
|
|
||||||
redirect('login.php');
|
redirect('/login.php');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ 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">
|
||||||
@@ -130,6 +137,59 @@ 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 -->
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli, "INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Sent', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id");
|
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");
|
||||||
|
|
||||||
|
|||||||
@@ -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(156);
|
$url_key = randomString(32);
|
||||||
|
|
||||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$ticket_prefix_esc', ticket_number = $ticket_number, ticket_source = 'Email', ticket_subject = '$subject', ticket_details = '$message_esc', ticket_priority = 'Low', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_url_key = '$url_key', ticket_client_id = $client_id");
|
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
19
db.sql
@@ -2441,6 +2441,25 @@ 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`
|
||||||
--
|
--
|
||||||
|
|||||||
242
functions.php
242
functions.php
@@ -4,20 +4,13 @@
|
|||||||
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($length = 16) {
|
function randomString(int $length = 16): string {
|
||||||
// Generate some cryptographically safe random bytes
|
$bytes = random_bytes((int) ceil($length * 3 / 4));
|
||||||
// Generate a little more than requested as we'll lose some later converting
|
return substr(
|
||||||
$random_bytes = random_bytes($length + 5);
|
rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='),
|
||||||
|
0,
|
||||||
// Convert the bytes to something somewhat human-readable
|
$length
|
||||||
$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
|
||||||
@@ -1443,6 +1436,10 @@ function appNotify($type, $details, $action = null, $client_id = 0, $entity_id =
|
|||||||
function logAction($type, $action, $description, $client_id = 0, $entity_id = 0) {
|
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;
|
||||||
}
|
}
|
||||||
@@ -1784,3 +1781,220 @@ function cleanupUnusedImages(string $html, string $folderFsPath, string $folderW
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple mysqli helper functions
|
||||||
|
* - Prepared statements under the hood
|
||||||
|
* - "Old style" INSERT/UPDATE SET feeling
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core executor: prepares, binds, executes.
|
||||||
|
*
|
||||||
|
* @throws Exception on error
|
||||||
|
*/
|
||||||
|
function dbExecute(mysqli $mysqli, string $sql, array $params = []): mysqli_stmt
|
||||||
|
{
|
||||||
|
$stmt = $mysqli->prepare($sql);
|
||||||
|
if (!$stmt) {
|
||||||
|
throw new Exception('MySQLi prepare error: ' . $mysqli->error . ' | SQL: ' . $sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($params)) {
|
||||||
|
$types = '';
|
||||||
|
$values = [];
|
||||||
|
|
||||||
|
foreach ($params as $param) {
|
||||||
|
if (is_int($param)) {
|
||||||
|
$types .= 'i';
|
||||||
|
} elseif (is_float($param)) {
|
||||||
|
$types .= 'd';
|
||||||
|
} elseif (is_bool($param)) {
|
||||||
|
$types .= 'i';
|
||||||
|
$param = $param ? 1 : 0;
|
||||||
|
} elseif (is_null($param)) {
|
||||||
|
$types .= 's';
|
||||||
|
$param = null;
|
||||||
|
} else {
|
||||||
|
$types .= 's';
|
||||||
|
}
|
||||||
|
$values[] = $param;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$stmt->bind_param($types, ...$values)) {
|
||||||
|
throw new Exception('MySQLi bind_param error: ' . $stmt->error . ' | SQL: ' . $sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$stmt->execute()) {
|
||||||
|
throw new Exception('MySQLi execute error: ' . $stmt->error . ' | SQL: ' . $sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all rows as associative arrays.
|
||||||
|
*/
|
||||||
|
function dbFetchAll(mysqli $mysqli, string $sql, array $params = []): array
|
||||||
|
{
|
||||||
|
$stmt = dbExecute($mysqli, $sql, $params);
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
if ($result === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return $result->fetch_all(MYSQLI_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single row (assoc) or null if none.
|
||||||
|
*/
|
||||||
|
function dbFetchOne(mysqli $mysqli, string $sql, array $params = []): ?array
|
||||||
|
{
|
||||||
|
$stmt = dbExecute($mysqli, $sql, $params);
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
if ($result === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
return $row !== null ? $row : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single scalar value (first column of first row) or null.
|
||||||
|
*/
|
||||||
|
function dbFetchValue(mysqli $mysqli, string $sql, array $params = [])
|
||||||
|
{
|
||||||
|
$row = dbFetchOne($mysqli, $sql, $params);
|
||||||
|
if ($row === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return reset($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INSERT using "SET" style.
|
||||||
|
* Example:
|
||||||
|
* $id = dbInsert($mysqli, 'clients', [
|
||||||
|
* 'client_name' => $name,
|
||||||
|
* 'client_type' => $type,
|
||||||
|
* ]);
|
||||||
|
*
|
||||||
|
* @return int insert_id
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function dbInsert(mysqli $mysqli, string $table, array $data): int
|
||||||
|
{
|
||||||
|
if (empty($data)) {
|
||||||
|
throw new InvalidArgumentException('dbInsert called with empty $data');
|
||||||
|
}
|
||||||
|
|
||||||
|
$setParts = [];
|
||||||
|
foreach ($data as $column => $_) {
|
||||||
|
$setParts[] = "$column = ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "INSERT INTO $table SET " . implode(', ', $setParts);
|
||||||
|
$params = array_values($data);
|
||||||
|
|
||||||
|
dbExecute($mysqli, $sql, $params);
|
||||||
|
|
||||||
|
return $mysqli->insert_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbUpdate(
|
||||||
|
mysqli $mysqli,
|
||||||
|
string $table,
|
||||||
|
array $data,
|
||||||
|
$where,
|
||||||
|
array $whereParams = []
|
||||||
|
): int {
|
||||||
|
if (empty($data)) {
|
||||||
|
throw new InvalidArgumentException('dbUpdate called with empty $data');
|
||||||
|
}
|
||||||
|
if (empty($where)) {
|
||||||
|
throw new InvalidArgumentException('dbUpdate requires a WHERE clause');
|
||||||
|
}
|
||||||
|
|
||||||
|
$setParts = [];
|
||||||
|
foreach ($data as $column => $_) {
|
||||||
|
$setParts[] = "$column = ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($where)) {
|
||||||
|
$whereParts = [];
|
||||||
|
$whereParams = [];
|
||||||
|
foreach ($where as $column => $value) {
|
||||||
|
$whereParts[] = "$column = ?";
|
||||||
|
$whereParams[] = $value;
|
||||||
|
}
|
||||||
|
$whereSql = implode(' AND ', $whereParts);
|
||||||
|
} else {
|
||||||
|
$whereSql = $where;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "UPDATE $table SET " . implode(', ', $setParts) . " WHERE $whereSql";
|
||||||
|
$params = array_merge(array_values($data), $whereParams);
|
||||||
|
|
||||||
|
$stmt = dbExecute($mysqli, $sql, $params);
|
||||||
|
return $stmt->affected_rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE helper.
|
||||||
|
*
|
||||||
|
* WHERE can be:
|
||||||
|
* - array: ['client_id' => $id] (auto "client_id = ?")
|
||||||
|
* - string: 'client_id = ?' (use with $whereParams)
|
||||||
|
*
|
||||||
|
* @return int affected_rows
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function dbDelete(
|
||||||
|
mysqli $mysqli,
|
||||||
|
string $table,
|
||||||
|
$where,
|
||||||
|
array $whereParams = []
|
||||||
|
): int {
|
||||||
|
if (empty($where)) {
|
||||||
|
throw new InvalidArgumentException('dbDelete requires a WHERE clause');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($where)) {
|
||||||
|
$whereParts = [];
|
||||||
|
$whereParams = [];
|
||||||
|
foreach ($where as $column => $value) {
|
||||||
|
$whereParts[] = "$column = ?";
|
||||||
|
$whereParams[] = $value;
|
||||||
|
}
|
||||||
|
$whereSql = implode(' AND ', $whereParts);
|
||||||
|
} else {
|
||||||
|
$whereSql = $where;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "DELETE FROM $table WHERE $whereSql";
|
||||||
|
$stmt = dbExecute($mysqli, $sql, $whereParams);
|
||||||
|
return $stmt->affected_rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction helpers (optional sugar).
|
||||||
|
*/
|
||||||
|
function dbBegin(mysqli $mysqli): void
|
||||||
|
{
|
||||||
|
$mysqli->begin_transaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbCommit(mysqli $mysqli): void
|
||||||
|
{
|
||||||
|
$mysqli->commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbRollback(mysqli $mysqli): void
|
||||||
|
{
|
||||||
|
$mysqli->rollback();
|
||||||
|
}
|
||||||
|
|||||||
114
guest/guest_approve_ticket_task.php
Normal file
114
guest/guest_approve_ticket_task.php
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "includes/inc_all_guest.php";
|
||||||
|
|
||||||
|
//Initialize the HTML Purifier to prevent XSS
|
||||||
|
require_once "../plugins/htmlpurifier/HTMLPurifier.standalone.php";
|
||||||
|
|
||||||
|
$purifier_config = HTMLPurifier_Config::createDefault();
|
||||||
|
$purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
|
||||||
|
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
|
||||||
|
$purifier = new HTMLPurifier($purifier_config);
|
||||||
|
|
||||||
|
if (!isset($_GET['task_approval_id'], $_GET['url_key'])) {
|
||||||
|
echo "<br><h2>Oops, something went wrong! Please raise a ticket if you believe this is an error.</h2>";
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Company info
|
||||||
|
$company_sql_row = mysqli_fetch_array(mysqli_query($mysqli, "
|
||||||
|
SELECT
|
||||||
|
company_phone,
|
||||||
|
company_phone_country_code,
|
||||||
|
company_website
|
||||||
|
FROM
|
||||||
|
companies,
|
||||||
|
settings
|
||||||
|
WHERE
|
||||||
|
companies.company_id = settings.company_id
|
||||||
|
AND companies.company_id = 1"
|
||||||
|
));
|
||||||
|
|
||||||
|
$company_phone_country_code = nullable_htmlentities($company_sql_row['company_phone_country_code']);
|
||||||
|
$company_phone = nullable_htmlentities(formatPhoneNumber($company_sql_row['company_phone'], $company_phone_country_code));
|
||||||
|
$company_website = nullable_htmlentities($company_sql_row['company_website']);
|
||||||
|
|
||||||
|
$approval_id = intval($_GET['task_approval_id']);
|
||||||
|
$url_key = sanitizeInput($_GET['url_key']);
|
||||||
|
|
||||||
|
$task_row = mysqli_fetch_assoc(mysqli_query($mysqli,
|
||||||
|
"SELECT * FROM task_approvals
|
||||||
|
LEFT JOIN tasks ON approval_task_id = task_id
|
||||||
|
LEFT JOIN tickets on task_ticket_id = ticket_id
|
||||||
|
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
|
||||||
|
WHERE approval_id = $approval_id AND approval_url_key = '$url_key'
|
||||||
|
LIMIT 1"
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$task_row) {
|
||||||
|
// Invalid
|
||||||
|
echo "<br><h2>Oops, something went wrong! Please raise a ticket if you believe this is an error.</h2>";
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$task_id = intval($task_row['task_id']);
|
||||||
|
$task_name = nullable_htmlentities($task_row['task_name']);
|
||||||
|
$approval_scope = nullable_htmlentities($task_row['approval_scope']);
|
||||||
|
$approval_type = nullable_htmlentities($task_row['approval_type']);
|
||||||
|
$approval_status = nullable_htmlentities($task_row['approval_status']);
|
||||||
|
|
||||||
|
$ticket_prefix = nullable_htmlentities($task_row['ticket_prefix']);
|
||||||
|
$ticket_number = intval($task_row['ticket_number']);
|
||||||
|
$ticket_status = nullable_htmlentities($task_row['ticket_status_name']);
|
||||||
|
$ticket_priority = nullable_htmlentities($task_row['ticket_priority']);
|
||||||
|
$ticket_subject = nullable_htmlentities($task_row['ticket_subject']);
|
||||||
|
$ticket_details = $purifier->purify($task_row['ticket_details']);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-dark text-center">
|
||||||
|
<h4 class="mt-1">
|
||||||
|
Task Approval for Ticket <?php echo $ticket_prefix, $ticket_number ?>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body prettyContent">
|
||||||
|
<h5><strong>Subject:</strong> <?php echo $ticket_subject ?></h5>
|
||||||
|
<p>
|
||||||
|
<strong>State:</strong> <?php echo $ticket_status ?>
|
||||||
|
<br>
|
||||||
|
<strong>Priority:</strong> <?php echo $ticket_priority ?>
|
||||||
|
<br>
|
||||||
|
</p>
|
||||||
|
<?php echo $ticket_details ?>
|
||||||
|
<hr>
|
||||||
|
<h5>Task Approval</h5>
|
||||||
|
<p>
|
||||||
|
<strong>Task Name: </strong><?= ucfirst($task_name); ?>
|
||||||
|
<br>
|
||||||
|
<strong>Scope/Type:</strong> <?= ucfirst($approval_scope) . " - " . ucfirst($approval_type)?>
|
||||||
|
<br>
|
||||||
|
<strong>Status:</strong> <?= ucfirst($approval_status)?>
|
||||||
|
<br>
|
||||||
|
<?php
|
||||||
|
if ($approval_status == 'pending') { ?>
|
||||||
|
<strong>Action: </strong><a href="guest_post.php?approve_ticket_task=<?= $task_id ?>&approval_id=<?= $approval_id ?>&approval_url_key=<?= $url_key ?>" class="confirm-link text-bold">Approve Task</a>
|
||||||
|
<?php } ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<?php echo "<i class='fas fa-phone fa-fw mr-2'></i>$company_phone | <i class='fas fa-globe fa-fw mr-2 ml-2'></i>$company_website"; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';
|
||||||
|
|
||||||
@@ -225,6 +225,39 @@ if (isset($_GET['add_ticket_feedback'], $_GET['url_key'])) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['approve_ticket_task'])) {
|
||||||
|
|
||||||
|
$task_id = intval($_GET['approve_ticket_task']);
|
||||||
|
$approval_id = intval($_GET['approval_id']);
|
||||||
|
$url_key = sanitizeInput($_GET['approval_url_key']);
|
||||||
|
|
||||||
|
$approval_row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending'"));
|
||||||
|
|
||||||
|
$task_name = nullable_htmlentities($approval_row['task_name']);
|
||||||
|
$scope = nullable_htmlentities($approval_row['approval_scope']);
|
||||||
|
$type = nullable_htmlentities($approval_row['approval_type']);
|
||||||
|
$required_user = intval($approval_row['approval_required_user_id']);
|
||||||
|
$created_by = intval($approval_row['approval_created_by']);
|
||||||
|
$ticket_id = intval($approval_row['task_ticket_id']);
|
||||||
|
|
||||||
|
if (!$approval_row) {
|
||||||
|
exit("Cannot find/approve that task");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approve
|
||||||
|
mysqli_query($mysqli, "UPDATE task_approvals SET approval_status = 'approved', approval_approved_by = $required_user WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending'");
|
||||||
|
|
||||||
|
// Notify tech
|
||||||
|
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = 'Guest approved ticket task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_user_id = $created_by");
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
logAction("Task", "Edit", "Guest user approved task $task_name via link (approval $approval_id)", 0, $task_id);
|
||||||
|
|
||||||
|
flash_alert("Task Approved");
|
||||||
|
redirect();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($_GET['export_quote_pdf'])) {
|
if (isset($_GET['export_quote_pdf'])) {
|
||||||
|
|
||||||
$quote_id = intval($_GET['export_quote_pdf']);
|
$quote_id = intval($_GET['export_quote_pdf']);
|
||||||
|
|||||||
@@ -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");
|
DEFINE("APP_VERSION", "25.12.1");
|
||||||
|
|||||||
@@ -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.8");
|
DEFINE("LATEST_DATABASE_VERSION", "2.3.9");
|
||||||
|
|||||||
882
login.php
882
login.php
@@ -1,329 +1,586 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Enforce a Content Security Policy for security against cross-site scripting
|
// Unified login (Agent + Client) using one email & password
|
||||||
|
|
||||||
header("Content-Security-Policy: default-src 'self'");
|
header("Content-Security-Policy: default-src 'self'");
|
||||||
|
|
||||||
// Check if the config.php file exists
|
|
||||||
if (!file_exists('config.php')) {
|
if (!file_exists('config.php')) {
|
||||||
// Redirect to the setup page if config.php doesn't exist
|
|
||||||
header("Location: /setup"); // Must use header as functions aren't included yet
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once "config.php";
|
|
||||||
|
|
||||||
// Check if setup mode is enabled or the variable is missing
|
|
||||||
if (!isset($config_enable_setup) || $config_enable_setup == 1) {
|
|
||||||
// Redirect to the setup page
|
|
||||||
header("Location: /setup");
|
header("Location: /setup");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Timezone
|
require_once "config.php";
|
||||||
require_once "includes/inc_set_timezone.php";
|
require_once "functions.php";
|
||||||
|
require_once "plugins/totp/totp.php";
|
||||||
|
|
||||||
// Check if the application is configured for HTTPS-only access
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
if ($config_https_only && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') && (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')) {
|
ini_set("session.cookie_httponly", true);
|
||||||
|
|
||||||
|
if ($config_https_only || !isset($config_https_only)) {
|
||||||
|
ini_set("session.cookie_secure", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($config_enable_setup) || $config_enable_setup == 1) {
|
||||||
|
header("Location: /setup");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$config_https_only
|
||||||
|
&& (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on')
|
||||||
|
&& (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')
|
||||||
|
) {
|
||||||
echo "Login is restricted as ITFlow defaults to HTTPS-only for enhanced security. To login using HTTP, modify the config.php file by setting config_https_only to false. However, this is strongly discouraged, especially when accessing from potentially unsafe networks like the internet.";
|
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 "functions.php";
|
require_once "includes/inc_set_timezone.php";
|
||||||
|
|
||||||
require_once "plugins/totp/totp.php";
|
|
||||||
|
|
||||||
|
|
||||||
// IP & User Agent for logging
|
|
||||||
$session_ip = sanitizeInput(getIP());
|
$session_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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query Settings for company
|
// Settings
|
||||||
$sql_settings = mysqli_query($mysqli, "SELECT * FROM settings LEFT JOIN companies ON settings.company_id = companies.company_id WHERE settings.company_id = 1");
|
$sql_settings = mysqli_query($mysqli, "
|
||||||
|
SELECT settings.*, companies.company_name, companies.company_logo
|
||||||
|
FROM settings
|
||||||
|
LEFT JOIN companies ON settings.company_id = companies.company_id
|
||||||
|
WHERE settings.company_id = 1
|
||||||
|
");
|
||||||
$row = mysqli_fetch_array($sql_settings);
|
$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'];
|
||||||
$config_smtp_username = $row['config_smtp_username'];
|
$config_smtp_username = $row['config_smtp_username'];
|
||||||
$config_smtp_password = $row['config_smtp_password'];
|
$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']);
|
|
||||||
|
|
||||||
// Login key (if setup)
|
|
||||||
$config_login_key_required = $row['config_login_key_required'];
|
|
||||||
$config_login_key_secret = $row['config_login_key_secret'];
|
|
||||||
|
|
||||||
|
$config_client_portal_enable = intval($row['config_client_portal_enable']);
|
||||||
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
|
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
|
||||||
|
|
||||||
// Login key verification
|
$config_login_key_required = $row['config_login_key_required'];
|
||||||
// If no/incorrect 'key' is supplied, send to client portal instead
|
$config_login_key_secret = $row['config_login_key_secret'];
|
||||||
if ($config_login_key_required) {
|
|
||||||
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
|
$azure_client_id = $row['config_azure_client_id'] ?? null;
|
||||||
redirect("client");
|
|
||||||
}
|
$response = null;
|
||||||
|
$token_field = null;
|
||||||
|
$show_role_choice = false;
|
||||||
|
|
||||||
|
$email = '';
|
||||||
|
$password = ''; // only ever used in the initial POST request
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
function pendingExpired($sess, $ttl_seconds = 120) {
|
||||||
|
return !$sess || empty($sess['created']) || (time() - intval($sess['created']) > $ttl_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP-Only cookies
|
// POST handling
|
||||||
ini_set("session.cookie_httponly", true);
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']) || isset($_POST['mfa_login']))) {
|
||||||
|
|
||||||
// Tell client to only send cookie(s) over HTTPS
|
$role_choice = $_POST['role_choice'] ?? null;
|
||||||
if ($config_https_only || !isset($config_https_only)) {
|
|
||||||
ini_set("session.cookie_secure", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle POST login request
|
$is_login_step = isset($_POST['login']);
|
||||||
if (isset($_POST['login'])) {
|
$is_role_step = isset($_POST['role_choice']) && !$is_login_step && !isset($_POST['mfa_login']);
|
||||||
|
$is_mfa_step = isset($_POST['mfa_login']);
|
||||||
|
|
||||||
// Sessions should start after the user has POSTed data
|
// -----------------------------------
|
||||||
session_start();
|
// STEP 2: ROLE CHOICE (no email/pass)
|
||||||
|
// -----------------------------------
|
||||||
|
if ($is_role_step) {
|
||||||
|
|
||||||
// Passed login brute force check
|
$posted_token = $_POST['pending_login_token'] ?? '';
|
||||||
$email = sanitizeInput($_POST['email']);
|
$sess = $_SESSION['pending_dual_login'] ?? null;
|
||||||
$password = $_POST['password'];
|
|
||||||
|
|
||||||
$current_code = 0; // Default value
|
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) {
|
||||||
if (isset($_POST['current_code'])) {
|
unset($_SESSION['pending_dual_login']);
|
||||||
$current_code = intval($_POST['current_code']);
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
}
|
$response = "
|
||||||
|
<div class='alert alert-danger'>
|
||||||
$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"));
|
Your login session expired. Please sign in again.
|
||||||
|
</div>";
|
||||||
// Check password
|
|
||||||
if ($row && password_verify($password, $row['user_password'])) {
|
|
||||||
|
|
||||||
// User password correct (partial login)
|
|
||||||
|
|
||||||
// Set temporary user variables
|
|
||||||
$user_name = sanitizeInput($row['user_name']);
|
|
||||||
$user_id = intval($row['user_id']);
|
|
||||||
$session_user_id = $user_id; // to pass the user_id to logAction function
|
|
||||||
$user_email = sanitizeInput($row['user_email']);
|
|
||||||
$token = sanitizeInput($row['user_token']);
|
|
||||||
$force_mfa = intval($row['user_config_force_mfa']);
|
|
||||||
$user_role_id = intval($row['user_role_id']);
|
|
||||||
$user_encryption_ciphertext = $row['user_specific_encryption_ciphertext'];
|
|
||||||
$user_extension_key = $row['user_extension_key'];
|
|
||||||
|
|
||||||
$mfa_is_complete = false; // Default to requiring MFA
|
|
||||||
$extended_log = ''; // Default value
|
|
||||||
|
|
||||||
if (empty($token)) {
|
|
||||||
// MFA is not configured
|
|
||||||
$mfa_is_complete = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate MFA via a remember-me cookie
|
|
||||||
if (isset($_COOKIE['rememberme'])) {
|
|
||||||
// Get remember tokens less than $config_login_remember_me_days_expire days old
|
|
||||||
$remember_tokens = mysqli_query($mysqli, "SELECT remember_token_token FROM remember_tokens WHERE remember_token_user_id = $user_id AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY)");
|
|
||||||
while ($row = mysqli_fetch_assoc($remember_tokens)) {
|
|
||||||
if (hash_equals($row['remember_token_token'], $_COOKIE['rememberme'])) {
|
|
||||||
$mfa_is_complete = true;
|
|
||||||
$extended_log = 'with 2FA remember-me cookie';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate MFA code
|
|
||||||
if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) {
|
|
||||||
$mfa_is_complete = true;
|
|
||||||
$extended_log = 'with MFA';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($mfa_is_complete) {
|
|
||||||
// MFA Completed successfully
|
|
||||||
|
|
||||||
// FULL LOGIN SUCCESS
|
|
||||||
|
|
||||||
// Create a remember me token, if requested
|
|
||||||
if (isset($_POST['remember_me'])) {
|
|
||||||
// TODO: Record the UA and IP a token is generated from so that can be shown later on
|
|
||||||
$newRememberToken = bin2hex(random_bytes(64));
|
|
||||||
setcookie('rememberme', $newRememberToken, time() + 86400*$config_login_remember_me_expire, "/", null, true, true);
|
|
||||||
mysqli_query($mysqli, "INSERT INTO remember_tokens SET remember_token_user_id = $user_id, remember_token_token = '$newRememberToken'");
|
|
||||||
|
|
||||||
$extended_log .= ", generated a new remember-me token";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check this login isn't suspicious
|
|
||||||
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(log_id) AS ip_previous_logins FROM logs WHERE log_type = 'Login' AND log_action = 'Success' AND log_ip = '$session_ip' AND log_user_id = $user_id"));
|
|
||||||
$ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']);
|
|
||||||
|
|
||||||
$sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(log_id) AS ua_previous_logins FROM logs WHERE log_type = 'Login' AND log_action = 'Success' AND log_user_agent = '$session_user_agent' AND log_user_id = $user_id"));
|
|
||||||
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
|
|
||||||
|
|
||||||
// Notify if both the user agent and IP are different
|
|
||||||
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
|
|
||||||
$subject = "$config_app_name new login for $user_name";
|
|
||||||
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
[
|
|
||||||
'from' => $config_mail_from_email,
|
|
||||||
'from_name' => $config_mail_from_name,
|
|
||||||
'recipient' => $user_email,
|
|
||||||
'recipient_name' => $user_name,
|
|
||||||
'subject' => $subject,
|
|
||||||
'body' => $body
|
|
||||||
]
|
|
||||||
];
|
|
||||||
addToMailQueue($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
|
|
||||||
|
|
||||||
// Session info
|
|
||||||
$_SESSION['user_id'] = $user_id;
|
|
||||||
$_SESSION['csrf_token'] = randomString(156);
|
|
||||||
$_SESSION['logged'] = true;
|
|
||||||
|
|
||||||
// Forcing MFA
|
|
||||||
if ($force_mfa == 1 && $token == NULL) {
|
|
||||||
$config_start_page = "user/mfa_enforcement.php";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup encryption session key
|
|
||||||
if (isset($user_encryption_ciphertext)) {
|
|
||||||
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
|
|
||||||
generateUserSessionKey($site_encryption_master_key);
|
|
||||||
|
|
||||||
// Setup extension - currently unused
|
|
||||||
//if (is_null($user_extension_key)) {
|
|
||||||
// Extension cookie
|
|
||||||
// Note: Browsers don't accept cookies with SameSite None if they are not HTTPS.
|
|
||||||
//setcookie("user_extension_key", "$user_extension_key", ['path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'None']);
|
|
||||||
|
|
||||||
// Set PHP session in DB, so we can access the session encryption data (above)
|
|
||||||
//$user_php_session = session_id();
|
|
||||||
//mysqli_query($mysqli, "UPDATE users SET user_php_session = '$user_php_session' WHERE user_id = $user_id");
|
|
||||||
//}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to last visited or config home
|
|
||||||
|
|
||||||
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
|
|
||||||
|
|
||||||
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']) );
|
|
||||||
|
|
||||||
} else {
|
|
||||||
redirect("agent/$config_start_page");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
$email = sanitizeInput($sess['email'] ?? '');
|
||||||
// MFA is configured and needs to be confirmed, or was unsuccessful
|
|
||||||
|
|
||||||
// HTML code for the token input field
|
|
||||||
$token_field = "
|
|
||||||
<div class='input-group mb-3'>
|
|
||||||
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6' class='form-control' placeholder='Enter your 2FA code' name='current_code' required autofocus>
|
|
||||||
<div class='input-group-append'>
|
|
||||||
<div class='input-group-text'>
|
|
||||||
<span class='fas fa-key'></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>";
|
|
||||||
|
|
||||||
// Log/notify if MFA was unsuccessful
|
|
||||||
if ($current_code !== 0) {
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logAction("Login", "MFA Failed", "$user_name failed MFA", 0, $user_id);
|
|
||||||
|
|
||||||
// Email the tech to advise their credentials may be compromised
|
|
||||||
if (!empty($config_smtp_host)) {
|
|
||||||
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
|
|
||||||
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
|
||||||
$data = [
|
|
||||||
[
|
|
||||||
'from' => $config_mail_from_email,
|
|
||||||
'from_name' => $config_mail_from_name,
|
|
||||||
'recipient' => $user_email,
|
|
||||||
'recipient_name' => $user_name,
|
|
||||||
'subject' => $subject,
|
|
||||||
'body' => $body
|
|
||||||
]
|
|
||||||
];
|
|
||||||
$mail = addToMailQueue($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTML feedback for incorrect 2FA code
|
|
||||||
$response = "
|
|
||||||
<div class='alert alert-warning'>
|
|
||||||
Please Enter 2FA Code!
|
|
||||||
<button class='close' data-dismiss='alert'>×</button>
|
|
||||||
</div>";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
// -----------------------------------
|
||||||
|
// STEP 3: MFA SUBMIT (no email/pass)
|
||||||
|
// -----------------------------------
|
||||||
|
if ($is_mfa_step && empty($response)) {
|
||||||
|
|
||||||
// Password incorrect or user doesn't exist - show generic error
|
$posted_token = $_POST['pending_mfa_token'] ?? '';
|
||||||
|
$sess = $_SESSION['pending_mfa_login'] ?? null;
|
||||||
|
|
||||||
header("HTTP/1.1 401 Unauthorized");
|
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) {
|
||||||
|
unset($_SESSION['pending_mfa_login']);
|
||||||
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
|
$response = "
|
||||||
|
<div class='alert alert-danger'>
|
||||||
|
Your MFA session expired. Please sign in again.
|
||||||
|
</div>";
|
||||||
|
} else {
|
||||||
|
$email = sanitizeInput($sess['email'] ?? '');
|
||||||
|
$role_choice = 'agent';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logAction("Login", "Failed", "Failed login attempt using $email");
|
// -----------------------------------
|
||||||
|
// STEP 1: INITIAL CREDENTIALS
|
||||||
|
// -----------------------------------
|
||||||
|
if ($is_login_step && empty($response)) {
|
||||||
|
$email = sanitizeInput($_POST['email'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
$response = "
|
if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
|
$response = "
|
||||||
<div class='alert alert-danger'>
|
<div class='alert alert-danger'>
|
||||||
Incorrect username or password.
|
Incorrect username or password.
|
||||||
<button class='close' data-dismiss='alert'>×</button>
|
|
||||||
</div>";
|
</div>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue only if no response error
|
||||||
|
if (empty($response)) {
|
||||||
|
|
||||||
|
// Query all possible matches for that email
|
||||||
|
$sql = mysqli_query($mysqli, "
|
||||||
|
SELECT users.*,
|
||||||
|
user_settings.*,
|
||||||
|
contacts.*,
|
||||||
|
clients.*
|
||||||
|
FROM users
|
||||||
|
LEFT JOIN user_settings ON users.user_id = user_settings.user_id
|
||||||
|
LEFT JOIN contacts ON users.user_id = contacts.contact_user_id
|
||||||
|
LEFT JOIN clients ON contacts.contact_client_id = clients.client_id
|
||||||
|
WHERE user_email = '$email'
|
||||||
|
AND user_archived_at IS NULL
|
||||||
|
AND user_status = 1
|
||||||
|
AND (
|
||||||
|
user_type = 1
|
||||||
|
OR (user_type = 2 AND client_archived_at IS NULL)
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
$agentRow = null;
|
||||||
|
$clientRow = null;
|
||||||
|
|
||||||
|
// Step 1: verify password. Step 2/3: use stored allowed ids.
|
||||||
|
$allowed_agent_id = null;
|
||||||
|
$allowed_client_id = null;
|
||||||
|
|
||||||
|
if ($is_role_step) {
|
||||||
|
$sess = $_SESSION['pending_dual_login'] ?? null;
|
||||||
|
$allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null;
|
||||||
|
$allowed_client_id = isset($sess['client_user_id']) ? intval($sess['client_user_id']) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_mfa_step) {
|
||||||
|
$sess = $_SESSION['pending_mfa_login'] ?? null;
|
||||||
|
$allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($r = mysqli_fetch_assoc($sql)) {
|
||||||
|
|
||||||
|
$ut = intval($r['user_type']);
|
||||||
|
|
||||||
|
if ($is_login_step) {
|
||||||
|
// Only Step 1 checks password
|
||||||
|
if (!password_verify($password, $r['user_password'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Step 2/3: restrict to ids we previously verified
|
||||||
|
if ($ut === 1 && $allowed_agent_id !== null && intval($r['user_id']) !== $allowed_agent_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($ut === 2 && $allowed_client_id !== null && intval($r['user_id']) !== $allowed_client_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ut === 1 && $agentRow === null) {
|
||||||
|
$agentRow = $r;
|
||||||
|
}
|
||||||
|
if ($ut === 2 && $clientRow === null) {
|
||||||
|
$clientRow = $r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($agentRow === null && $clientRow === null) {
|
||||||
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
|
logAction("Login", "Failed", "Failed login attempt using $email");
|
||||||
|
$response = "
|
||||||
|
<div class='alert alert-danger'>
|
||||||
|
Incorrect username or password.
|
||||||
|
</div>";
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$selectedRow = null;
|
||||||
|
$selectedType = null; // 1 agent, 2 client
|
||||||
|
|
||||||
|
// Dual role
|
||||||
|
if ($agentRow !== null && $clientRow !== null) {
|
||||||
|
|
||||||
|
if ($role_choice === 'agent') {
|
||||||
|
$selectedRow = $agentRow;
|
||||||
|
$selectedType = 1;
|
||||||
|
} elseif ($role_choice === 'client') {
|
||||||
|
$selectedRow = $clientRow;
|
||||||
|
$selectedType = 2;
|
||||||
|
} else {
|
||||||
|
// Show role choice screen
|
||||||
|
$show_role_choice = true;
|
||||||
|
|
||||||
|
// If this is the first time (Step 1), we need to stash allowed ids and (optional) decrypted agent encryption key
|
||||||
|
// WITHOUT storing password.
|
||||||
|
if ($is_login_step) {
|
||||||
|
|
||||||
|
$pending_token = bin2hex(random_bytes(32));
|
||||||
|
|
||||||
|
// If agent has user-specific encryption ciphertext, decrypt it NOW while password is present.
|
||||||
|
$agent_master_key = null;
|
||||||
|
$agent_cipher = $agentRow['user_specific_encryption_ciphertext'] ?? null;
|
||||||
|
if (!empty($agent_cipher)) {
|
||||||
|
$agent_master_key = decryptUserSpecificKey($agent_cipher, $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['pending_dual_login'] = [
|
||||||
|
'email' => $email,
|
||||||
|
'agent_user_id' => intval($agentRow['user_id']),
|
||||||
|
'client_user_id' => intval($clientRow['user_id']),
|
||||||
|
'agent_master_key' => $agent_master_key, // may be null
|
||||||
|
'token' => $pending_token,
|
||||||
|
'created' => time()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Single role
|
||||||
|
if ($agentRow !== null) {
|
||||||
|
$selectedRow = $agentRow;
|
||||||
|
$selectedType = 1;
|
||||||
|
} else {
|
||||||
|
$selectedRow = $clientRow;
|
||||||
|
$selectedType = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed if selected
|
||||||
|
if ($selectedRow !== null && $selectedType !== null) {
|
||||||
|
|
||||||
|
// Clear dual pending once we actually proceed
|
||||||
|
unset($_SESSION['pending_dual_login']);
|
||||||
|
|
||||||
|
$user_id = intval($selectedRow['user_id']);
|
||||||
|
$user_email = sanitizeInput($selectedRow['user_email']);
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// AGENT FLOW
|
||||||
|
// =========================
|
||||||
|
if ($selectedType === 1) {
|
||||||
|
|
||||||
|
if ($config_login_key_required) {
|
||||||
|
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
|
||||||
|
redirect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_name = sanitizeInput($selectedRow['user_name']);
|
||||||
|
$token = sanitizeInput($selectedRow['user_token']);
|
||||||
|
$force_mfa = intval($selectedRow['user_config_force_mfa']);
|
||||||
|
$user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext'];
|
||||||
|
|
||||||
|
$current_code = 0;
|
||||||
|
if (isset($_POST['current_code'])) {
|
||||||
|
$current_code = intval($_POST['current_code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mfa_is_complete = false;
|
||||||
|
$extended_log = '';
|
||||||
|
|
||||||
|
if (empty($token)) {
|
||||||
|
$mfa_is_complete = true; // no MFA configured
|
||||||
|
}
|
||||||
|
|
||||||
|
// remember-me cookie allows bypass
|
||||||
|
if (isset($_COOKIE['rememberme'])) {
|
||||||
|
$remember_tokens = mysqli_query($mysqli, "
|
||||||
|
SELECT remember_token_token
|
||||||
|
FROM remember_tokens
|
||||||
|
WHERE remember_token_user_id = $user_id
|
||||||
|
AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY)
|
||||||
|
");
|
||||||
|
while ($remember_row = mysqli_fetch_assoc($remember_tokens)) {
|
||||||
|
if (hash_equals($remember_row['remember_token_token'], $_COOKIE['rememberme'])) {
|
||||||
|
$mfa_is_complete = true;
|
||||||
|
$extended_log = 'with 2FA remember-me cookie';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate MFA code
|
||||||
|
if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) {
|
||||||
|
$mfa_is_complete = true;
|
||||||
|
$extended_log = 'with MFA';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mfa_is_complete) {
|
||||||
|
|
||||||
|
// Clear pending MFA if exists
|
||||||
|
unset($_SESSION['pending_mfa_login']);
|
||||||
|
|
||||||
|
// Remember me token creation
|
||||||
|
if (isset($_POST['remember_me'])) {
|
||||||
|
$newRememberToken = bin2hex(random_bytes(64));
|
||||||
|
setcookie('rememberme', $newRememberToken, time() + 86400 * $config_login_remember_me_expire, "/", null, true, true);
|
||||||
|
|
||||||
|
mysqli_query($mysqli, "
|
||||||
|
INSERT INTO remember_tokens
|
||||||
|
SET remember_token_user_id = $user_id,
|
||||||
|
remember_token_token = '$newRememberToken'
|
||||||
|
");
|
||||||
|
$extended_log .= ", generated a new remember-me token";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suspicious login checks / email notify (kept from your code)
|
||||||
|
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
||||||
|
SELECT COUNT(log_id) AS ip_previous_logins
|
||||||
|
FROM logs
|
||||||
|
WHERE log_type = 'Login'
|
||||||
|
AND log_action = 'Success'
|
||||||
|
AND log_ip = '$session_ip'
|
||||||
|
AND log_user_id = $user_id
|
||||||
|
"));
|
||||||
|
$ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']);
|
||||||
|
|
||||||
|
$sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
||||||
|
SELECT COUNT(log_id) AS ua_previous_logins
|
||||||
|
FROM logs
|
||||||
|
WHERE log_type = 'Login'
|
||||||
|
AND log_action = 'Success'
|
||||||
|
AND log_user_agent = '$session_user_agent'
|
||||||
|
AND log_user_id = $user_id
|
||||||
|
"));
|
||||||
|
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
|
||||||
|
|
||||||
|
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
|
||||||
|
$subject = "$config_app_name new login for $user_name";
|
||||||
|
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
||||||
|
|
||||||
|
$data = [[
|
||||||
|
'from' => $config_mail_from_email,
|
||||||
|
'from_name' => $config_mail_from_name,
|
||||||
|
'recipient' => $user_email,
|
||||||
|
'recipient_name' => $user_name,
|
||||||
|
'subject' => $subject,
|
||||||
|
'body' => $body
|
||||||
|
]];
|
||||||
|
addToMailQueue($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
|
||||||
|
|
||||||
|
$_SESSION['user_id'] = $user_id;
|
||||||
|
$_SESSION['csrf_token'] = randomString(32);
|
||||||
|
$_SESSION['logged'] = true;
|
||||||
|
|
||||||
|
if ($force_mfa == 1 && $token == NULL) {
|
||||||
|
$config_start_page = "user/mfa_enforcement.php";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Setup encryption session key WITHOUT PASSWORD IN SESSION
|
||||||
|
// If we are coming from MFA step, master key is in pending_mfa_login.
|
||||||
|
// If we are coming from login step with no MFA, decrypt now.
|
||||||
|
$site_encryption_master_key = null;
|
||||||
|
|
||||||
|
if ($is_mfa_step) {
|
||||||
|
$sess = $_SESSION['pending_mfa_login'] ?? null;
|
||||||
|
if ($sess && isset($sess['agent_master_key'])) {
|
||||||
|
$site_encryption_master_key = $sess['agent_master_key'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No MFA step: password exists in this request (Step 1)
|
||||||
|
if (!empty($user_encryption_ciphertext)) {
|
||||||
|
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($site_encryption_master_key)) {
|
||||||
|
generateUserSessionKey($site_encryption_master_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect
|
||||||
|
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
|
||||||
|
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']));
|
||||||
|
} else {
|
||||||
|
redirect("agent/$config_start_page");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// MFA required — store *only what we need*, not password
|
||||||
|
$pending_mfa_token = bin2hex(random_bytes(32));
|
||||||
|
|
||||||
|
// If we arrived here from role-choice step, the agent master key may be in pending_dual_login
|
||||||
|
// If we arrived from initial login step, decrypt now (password in memory) and store master key.
|
||||||
|
$agent_master_key = null;
|
||||||
|
|
||||||
|
if ($is_role_step) {
|
||||||
|
$sess = $_SESSION['pending_dual_login'] ?? null;
|
||||||
|
if ($sess && isset($sess['agent_master_key'])) {
|
||||||
|
$agent_master_key = $sess['agent_master_key'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!empty($user_encryption_ciphertext)) {
|
||||||
|
$agent_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['pending_mfa_login'] = [
|
||||||
|
'email' => $user_email,
|
||||||
|
'agent_user_id' => $user_id,
|
||||||
|
'agent_master_key'=> $agent_master_key, // may be null
|
||||||
|
'token' => $pending_mfa_token,
|
||||||
|
'created' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
$token_field = "
|
||||||
|
<div class='input-group mb-3'>
|
||||||
|
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6'
|
||||||
|
class='form-control' placeholder='Verify your 2FA code'
|
||||||
|
name='current_code' required autofocus>
|
||||||
|
<div class='input-group-append'>
|
||||||
|
<div class='input-group-text'>
|
||||||
|
<span class='fas fa-key'></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>";
|
||||||
|
|
||||||
|
if ($current_code !== 0) {
|
||||||
|
logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id);
|
||||||
|
|
||||||
|
if (!empty($config_smtp_host)) {
|
||||||
|
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
|
||||||
|
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
||||||
|
$data = [[
|
||||||
|
'from' => $config_mail_from_email,
|
||||||
|
'from_name' => $config_mail_from_name,
|
||||||
|
'recipient' => $user_email,
|
||||||
|
'recipient_name' => $user_name,
|
||||||
|
'subject' => $subject,
|
||||||
|
'body' => $body
|
||||||
|
]];
|
||||||
|
addToMailQueue($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = "
|
||||||
|
<div class='alert alert-danger'>
|
||||||
|
Please enter a valid 2FA code.
|
||||||
|
</div>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// CLIENT FLOW
|
||||||
|
// =========================
|
||||||
|
} elseif ($selectedType === 2) {
|
||||||
|
|
||||||
|
if ($config_client_portal_enable != 1) {
|
||||||
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
|
logAction("Client Login", "Failed", "Client portal disabled; login attempt using $email");
|
||||||
|
$response = "
|
||||||
|
<div class='alert alert-danger'>
|
||||||
|
Incorrect username or password.
|
||||||
|
</div>";
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$user_id = intval($selectedRow['contact_user_id']);
|
||||||
|
$client_id = intval($selectedRow['contact_client_id']);
|
||||||
|
$contact_id = intval($selectedRow['contact_id']);
|
||||||
|
$user_auth_method = sanitizeInput($selectedRow['user_auth_method']);
|
||||||
|
|
||||||
|
if ($client_id && $contact_id && $user_auth_method === 'local') {
|
||||||
|
|
||||||
|
$_SESSION['client_logged_in'] = true;
|
||||||
|
$_SESSION['client_id'] = $client_id;
|
||||||
|
$_SESSION['user_id'] = $user_id;
|
||||||
|
$_SESSION['user_type'] = 2;
|
||||||
|
$_SESSION['contact_id'] = $contact_id;
|
||||||
|
$_SESSION['login_method'] = "local";
|
||||||
|
|
||||||
|
logAction("Client Login", "Success", "Client contact $user_email successfully logged in locally", $client_id, $user_id);
|
||||||
|
|
||||||
|
header("Location: client/index.php");
|
||||||
|
exit();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id);
|
||||||
|
|
||||||
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
|
$response = "
|
||||||
|
<div class='alert alert-danger'>
|
||||||
|
Incorrect username or password.
|
||||||
|
</div>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
// Form state
|
||||||
|
$show_mfa_form = (isset($token_field) && !empty($token_field));
|
||||||
|
$show_login_form = (!$show_role_choice && !$show_mfa_form);
|
||||||
|
|
||||||
|
?>
|
||||||
<!DOCTYPE html>
|
<!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">
|
||||||
|
|
||||||
@@ -336,83 +593,108 @@ if (isset($_POST['login'])) {
|
|||||||
<?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">
|
||||||
|
|
||||||
<?php if (!empty($config_login_message)){ ?>
|
<?php if (!empty($config_login_message)){ ?>
|
||||||
<p class="login-box-msg px-0"><?php echo nl2br($config_login_message); ?></p>
|
<p class="login-box-msg px-0"><?php echo nl2br($config_login_message); ?></p>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<?php if (isset($response)) { ?>
|
<?php if (isset($response)) { ?>
|
||||||
<p><?php echo $response; ?></p>
|
<p><?php echo $response; ?></p>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
|
||||||
<div class="input-group mb-3" <?php if (isset($token_field)) { echo "hidden"; } ?>>
|
<?php if ($show_login_form): ?>
|
||||||
<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"; } ?> >
|
<!-- STEP 1: Email + Password -->
|
||||||
<div class="input-group-append">
|
<div class="input-group mb-3">
|
||||||
<div class="input-group-text">
|
<input type="text" class="form-control"
|
||||||
<span class="fas fa-envelope"></span>
|
placeholder="<?php if ($config_login_key_required) { if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { echo "Client "; } } echo "Email"; ?>"
|
||||||
|
name="email"
|
||||||
|
value="<?php echo htmlspecialchars($email ?? '', ENT_QUOTES); ?>"
|
||||||
|
required autofocus
|
||||||
|
>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<span class="fas fa-envelope"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
|
||||||
if (isset($token_field)) {
|
<?php endif; ?>
|
||||||
|
|
||||||
echo $token_field;
|
<?php if ($show_role_choice): ?>
|
||||||
?>
|
<!-- STEP 2: Role choice only -->
|
||||||
|
<input type="hidden" name="pending_login_token"
|
||||||
|
value="<?php echo htmlspecialchars($_SESSION['pending_dual_login']['token'] ?? '', ENT_QUOTES); ?>">
|
||||||
|
|
||||||
<div class="form-group mb-3">
|
<div class="mb-2 text-center">
|
||||||
<div class="custom-control custom-checkbox">
|
<button type="submit" class="btn btn-dark btn-block mb-2" name="role_choice" value="agent">
|
||||||
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
|
Log in as Agent
|
||||||
<label class="custom-control-label" for="remember_me">Remember Me</label>
|
</button>
|
||||||
|
<button type="submit" class="btn btn-light btn-block" name="role_choice" value="client">
|
||||||
|
Log in as Client
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php if ($show_mfa_form): ?>
|
||||||
|
<!-- STEP 3: MFA only -->
|
||||||
|
<?php echo $token_field; ?>
|
||||||
|
|
||||||
}
|
<input type="hidden" name="pending_mfa_token"
|
||||||
|
value="<?php echo htmlspecialchars($_SESSION['pending_mfa_login']['token'] ?? '', ENT_QUOTES); ?>">
|
||||||
|
|
||||||
?>
|
<div class="form-group mb-3">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
|
||||||
|
<label class="custom-control-label" for="remember_me">Remember Me</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
|
<button type="submit" class="btn btn-dark btn-block mb-3" name="mfa_login">Verify & Sign In</button>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if($config_client_portal_enable == 1){ ?>
|
|
||||||
<hr>
|
|
||||||
<h5 class="text-center">Looking for the <a href="client">Client Portal?<a/></h5>
|
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<?php if($config_client_portal_enable == 1){ ?>
|
||||||
|
<hr>
|
||||||
|
<?php if (!empty($config_smtp_host)) { ?>
|
||||||
|
<a href="client/login_reset.php">Forgot password?</a>
|
||||||
|
<?php } ?>
|
||||||
|
<?php if (!empty($azure_client_id)) { ?>
|
||||||
|
<div class="col text-center mt-2">
|
||||||
|
<a href="client/login_microsoft.php">
|
||||||
|
<button type="button" class="btn btn-secondary">Login with Microsoft Entra</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- /.login-card-body -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /.login-box -->
|
|
||||||
|
|
||||||
<!-- jQuery -->
|
<?php
|
||||||
|
if (!$config_whitelabel_enabled) {
|
||||||
|
echo '<small class="text-muted">Powered by ITFlow</small>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<script src="plugins/jquery/jquery.min.js"></script>
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user