Compare commits

...

14 Commits

Author SHA1 Message Date
Johnny 163aa3062e
Merge pull request #1254 from itflow-org/develop
Develop to Master for 25.12.1 Maint Release
2025-12-14 14:03:56 -05:00
johnnyq 32f996d034 If login key is set and it is not provided show Client Email instead of just Email for placeholder 2025-12-14 13:42:38 -05:00
johnnyq 312eb4dffc Allow use of login key only for agents 2025-12-14 13:16:54 -05:00
johnnyq 1916456c84 Fix White Label not displaying on the login page 2025-12-14 13:04:53 -05:00
johnnyq 9b8d37b577 Updated changelog 2025-12-13 15:47:28 -05:00
johnnyq 05018e5f17 Added Reset favicon 2025-12-13 15:45:02 -05:00
johnnyq 72ef918452 Update changelog and bump app version to 25.12.1 2025-12-12 16:51:34 -05:00
johnnyq 27fde82aff Fixed Adding Payment provider not adding an account, now adding you can customize the income/expense account, expense category, and Expense vendor. Moved Saved Payment Provider Methods into Payment Providers as a link instead of on the admin side nav. Same with AI Provider and AI Models. 2025-12-12 16:42:09 -05:00
johnnyq b27ffe6635 Refine DB Helpers 2025-12-10 18:32:46 -05:00
johnnyq 84cc4a094a Add DB helpers to make MySQLi Prepared statements less bloated and require less code 2025-12-10 17:09:34 -05:00
johnnyq e75600ee05 Fix New ticket button in contact details in the related tickets section 2025-12-10 13:18:41 -05:00
johnnyq 871ad2ea7e Update legacy client login links to use the new unified login method 2025-12-10 12:21:12 -05:00
wrongecho 8b5f2e0f3f Update + credit 2025-12-10 08:54:24 +00:00
johnnyq 58d6ab7342 Unify Agent and Client login, if same user exists as a client and an agent then offer a selection of client portal or agent portal 2025-12-09 13:39:16 -05:00
23 changed files with 1103 additions and 723 deletions

View File

@ -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.

View File

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

View File

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

View File

@ -39,7 +39,7 @@ $num_rows = mysqli_num_rows($sql);
Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?> Key <?php if ($sort == 'ai_provider_api_key') { echo $order_icon; } ?>
</a> </a>
</th> </th>
<th> <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">

View File

@ -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">

View File

@ -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>

View File

@ -10,10 +10,10 @@ $row = mysqli_fetch_array($sql);
$provider_name = nullable_htmlentities($row['payment_provider_name']); $provider_name = nullable_htmlentities($row['payment_provider_name']);
$public_key = nullable_htmlentities($row['payment_provider_public_key']); $public_key = nullable_htmlentities($row['payment_provider_public_key']);
$private_key = nullable_htmlentities($row['payment_provider_private_key']); $private_key = nullable_htmlentities($row['payment_provider_private_key']);
$account_id = 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>&times;</span> <span>&times;</span>
</button> </button>
</div> </div>
<form action="post.php" method="post" autocomplete="off"> <form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?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>

View File

@ -6,10 +6,10 @@ $order = "ASC";
require_once "includes/inc_all_admin.php"; require_once "includes/inc_all_admin.php";
$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers $sql = mysqli_query($mysqli, "SELECT * FROM payment_providers
LEFT JOIN accounts ON payment_provider_account = account_id LEFT JOIN accounts ON payment_provider_account = account_id
LEFT JOIN vendors ON payment_provider_expense_vendor = vendor_id LEFT JOIN vendors ON payment_provider_expense_vendor = vendor_id
LEFT JOIN categories ON payment_provider_expense_category = category_id LEFT JOIN categories ON payment_provider_expense_category = category_id
ORDER BY $sort $order" ORDER BY $sort $order"
); );
@ -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">

View File

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

View File

@ -14,53 +14,20 @@ if (isset($_POST['add_payment_provider'])) {
$public_key = sanitizeInput($_POST['public_key']); $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");
@ -98,7 +67,7 @@ if (isset($_POST['edit_payment_provider'])) {
if (isset($_GET['delete_payment_provider'])) { if (isset($_GET['delete_payment_provider'])) {
validateCSRFToken($_GET['csrf_token']); validateCSRFToken($_GET['csrf_token']);
$provider_id = intval($_GET['delete_payment_provider']); $provider_id = intval($_GET['delete_payment_provider']);
// When deleted it cascades deletes // When deleted it cascades deletes

View File

@ -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();
}

View File

@ -13,18 +13,18 @@ $sql = mysqli_query($mysqli, "
clients.client_name, clients.client_name,
client_payment_provider.payment_provider_client client_payment_provider.payment_provider_client
FROM client_saved_payment_methods FROM client_saved_payment_methods
LEFT JOIN payment_providers LEFT JOIN payment_providers
ON client_saved_payment_methods.saved_payment_provider_id = payment_providers.payment_provider_id ON client_saved_payment_methods.saved_payment_provider_id = payment_providers.payment_provider_id
LEFT JOIN clients LEFT JOIN clients
ON client_saved_payment_methods.saved_payment_client_id = clients.client_id ON client_saved_payment_methods.saved_payment_client_id = clients.client_id
LEFT JOIN client_payment_provider LEFT JOIN client_payment_provider
ON client_payment_provider.client_id = client_saved_payment_methods.saved_payment_client_id ON client_payment_provider.client_id = client_saved_payment_methods.saved_payment_client_id
AND client_payment_provider.payment_provider_id = client_saved_payment_methods.saved_payment_provider_id AND client_payment_provider.payment_provider_id = client_saved_payment_methods.saved_payment_provider_id
WHERE WHERE
client_name LIKE '%$q%' client_name LIKE '%$q%'
OR payment_provider_name LIKE '%$q%' OR payment_provider_name LIKE '%$q%'
OR saved_payment_description LIKE '%$q%' OR saved_payment_description LIKE '%$q%'
OR payment_provider_client LIKE '%$q%' OR payment_provider_client LIKE '%$q%'
OR saved_payment_provider_method LIKE '%$q%' OR saved_payment_provider_method LIKE '%$q%'
ORDER BY $sort $order ORDER BY $sort $order
"); ");
@ -33,6 +33,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?> ?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="/admin">Admin</a>
</li>
<li class="breadcrumb-item">
<a href="payment_provider.php">Payment Providers</a>
</li>
<li class="breadcrumb-item active">Saved Payment Methods (Stripe)</li>
</ol>
<div class="card card-dark"> <div class="card 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>

View File

@ -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";

View File

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

View File

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

View File

@ -16,13 +16,13 @@ if (!isset($_SESSION)) {
} }
if (!isset($_SESSION['client_logged_in']) || !$_SESSION['client_logged_in']) { if (!isset($_SESSION['client_logged_in']) || !$_SESSION['client_logged_in']) {
header("Location: /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();
} }

View File

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

View File

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

View File

@ -12,7 +12,7 @@ require_once '../includes/load_global_settings.php';
if (empty($config_smtp_host)) { if (empty($config_smtp_host)) {
header("Location: login.php"); header("Location: /login.php");
exit(); exit();
} }
@ -157,7 +157,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") {
// Redirect to login page // Redirect to login page
$_SESSION['login_message'] = "Password reset successfully!"; $_SESSION['login_message'] = "Password reset successfully!";
header("Location: login.php"); header("Location: /login.php");
exit(); exit();
} else { } else {
@ -275,7 +275,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") {
?> ?>
</p> </p>
<a href="login.php">Back to login</a> <a href="/login.php">Back to login</a>
</div> </div>

View File

@ -320,7 +320,7 @@ if (isset($_GET['logout'])) {
session_unset(); session_unset();
session_destroy(); session_destroy();
redirect('login.php'); redirect('/login.php');
} }

View File

@ -1784,3 +1784,220 @@ function cleanupUnusedImages(string $html, string $folderFsPath, string $folderW
} }
} }
} }
/**
* Simple mysqli helper functions
* - Prepared statements under the hood
* - "Old style" INSERT/UPDATE SET feeling
*/
/**
* Core executor: prepares, binds, executes.
*
* @throws Exception on error
*/
function dbExecute(mysqli $mysqli, string $sql, array $params = []): mysqli_stmt
{
$stmt = $mysqli->prepare($sql);
if (!$stmt) {
throw new Exception('MySQLi prepare error: ' . $mysqli->error . ' | SQL: ' . $sql);
}
if (!empty($params)) {
$types = '';
$values = [];
foreach ($params as $param) {
if (is_int($param)) {
$types .= 'i';
} elseif (is_float($param)) {
$types .= 'd';
} elseif (is_bool($param)) {
$types .= 'i';
$param = $param ? 1 : 0;
} elseif (is_null($param)) {
$types .= 's';
$param = null;
} else {
$types .= 's';
}
$values[] = $param;
}
if (!$stmt->bind_param($types, ...$values)) {
throw new Exception('MySQLi bind_param error: ' . $stmt->error . ' | SQL: ' . $sql);
}
}
if (!$stmt->execute()) {
throw new Exception('MySQLi execute error: ' . $stmt->error . ' | SQL: ' . $sql);
}
return $stmt;
}
/**
* Fetch all rows as associative arrays.
*/
function dbFetchAll(mysqli $mysqli, string $sql, array $params = []): array
{
$stmt = dbExecute($mysqli, $sql, $params);
$result = $stmt->get_result();
if ($result === false) {
return [];
}
return $result->fetch_all(MYSQLI_ASSOC);
}
/**
* Fetch a single row (assoc) or null if none.
*/
function dbFetchOne(mysqli $mysqli, string $sql, array $params = []): ?array
{
$stmt = dbExecute($mysqli, $sql, $params);
$result = $stmt->get_result();
if ($result === false) {
return null;
}
$row = $result->fetch_assoc();
return $row !== null ? $row : null;
}
/**
* Fetch a single scalar value (first column of first row) or null.
*/
function dbFetchValue(mysqli $mysqli, string $sql, array $params = [])
{
$row = dbFetchOne($mysqli, $sql, $params);
if ($row === null) {
return null;
}
return reset($row);
}
/**
* INSERT using "SET" style.
* Example:
* $id = dbInsert($mysqli, 'clients', [
* 'client_name' => $name,
* 'client_type' => $type,
* ]);
*
* @return int insert_id
*
* @throws InvalidArgumentException
* @throws Exception
*/
function dbInsert(mysqli $mysqli, string $table, array $data): int
{
if (empty($data)) {
throw new InvalidArgumentException('dbInsert called with empty $data');
}
$setParts = [];
foreach ($data as $column => $_) {
$setParts[] = "$column = ?";
}
$sql = "INSERT INTO $table SET " . implode(', ', $setParts);
$params = array_values($data);
dbExecute($mysqli, $sql, $params);
return $mysqli->insert_id;
}
function dbUpdate(
mysqli $mysqli,
string $table,
array $data,
$where,
array $whereParams = []
): int {
if (empty($data)) {
throw new InvalidArgumentException('dbUpdate called with empty $data');
}
if (empty($where)) {
throw new InvalidArgumentException('dbUpdate requires a WHERE clause');
}
$setParts = [];
foreach ($data as $column => $_) {
$setParts[] = "$column = ?";
}
if (is_array($where)) {
$whereParts = [];
$whereParams = [];
foreach ($where as $column => $value) {
$whereParts[] = "$column = ?";
$whereParams[] = $value;
}
$whereSql = implode(' AND ', $whereParts);
} else {
$whereSql = $where;
}
$sql = "UPDATE $table SET " . implode(', ', $setParts) . " WHERE $whereSql";
$params = array_merge(array_values($data), $whereParams);
$stmt = dbExecute($mysqli, $sql, $params);
return $stmt->affected_rows;
}
/**
* DELETE helper.
*
* WHERE can be:
* - array: ['client_id' => $id] (auto "client_id = ?")
* - string: 'client_id = ?' (use with $whereParams)
*
* @return int affected_rows
*
* @throws InvalidArgumentException
* @throws Exception
*/
function dbDelete(
mysqli $mysqli,
string $table,
$where,
array $whereParams = []
): int {
if (empty($where)) {
throw new InvalidArgumentException('dbDelete requires a WHERE clause');
}
if (is_array($where)) {
$whereParts = [];
$whereParams = [];
foreach ($where as $column => $value) {
$whereParts[] = "$column = ?";
$whereParams[] = $value;
}
$whereSql = implode(' AND ', $whereParts);
} else {
$whereSql = $where;
}
$sql = "DELETE FROM $table WHERE $whereSql";
$stmt = dbExecute($mysqli, $sql, $whereParams);
return $stmt->affected_rows;
}
/**
* Transaction helpers (optional sugar).
*/
function dbBegin(mysqli $mysqli): void
{
$mysqli->begin_transaction();
}
function dbCommit(mysqli $mysqli): void
{
$mysqli->commit();
}
function dbRollback(mysqli $mysqli): void
{
$mysqli->rollback();
}

View File

@ -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");

698
login.php
View File

@ -1,5 +1,7 @@
<?php <?php
// Unified login (Agent + Client) using one email & password
// Enforce a Content Security Policy for security against cross-site scripting // Enforce a Content Security Policy for security against cross-site scripting
header("Content-Security-Policy: default-src 'self'"); header("Content-Security-Policy: default-src 'self'");
@ -11,6 +13,21 @@ if (!file_exists('config.php')) {
} }
require_once "config.php"; require_once "config.php";
require_once "functions.php";
require_once "plugins/totp/totp.php";
// Sessions & cookies
if (session_status() === PHP_SESSION_NONE) {
// HTTP-Only cookies
ini_set("session.cookie_httponly", true);
// Tell client to only send cookie(s) over HTTPS
if ($config_https_only || !isset($config_https_only)) {
ini_set("session.cookie_secure", true);
}
session_start();
}
// Check if setup mode is enabled or the variable is missing // Check if setup mode is enabled or the variable is missing
if (!isset($config_enable_setup) || $config_enable_setup == 1) { if (!isset($config_enable_setup) || $config_enable_setup == 1) {
@ -19,28 +36,30 @@ if (!isset($config_enable_setup) || $config_enable_setup == 1) {
exit(); exit();
} }
// Set Timezone
require_once "includes/inc_set_timezone.php";
// Check if the application is configured for HTTPS-only access // Check if the application is configured for HTTPS-only access
if ($config_https_only && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') && (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')) { if ($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"; // Set Timezone after session_start
require_once "includes/inc_set_timezone.php";
require_once "plugins/totp/totp.php";
// IP & User Agent for logging // 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 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 // 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) {
@ -53,253 +72,402 @@ if ($failed_login_count >= 15) {
} }
// Query Settings for company // Query Settings for company
$sql_settings = mysqli_query($mysqli, "SELECT * FROM settings LEFT JOIN companies ON settings.company_id = companies.company_id WHERE settings.company_id = 1"); $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 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 // 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 // Client Portal Enabled
$config_client_portal_enable = intval($row['config_client_portal_enable']); $config_client_portal_enable = intval($row['config_client_portal_enable']);
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
// Login key (if setup) // Login key (if setup)
$config_login_key_required = $row['config_login_key_required']; $config_login_key_required = $row['config_login_key_required'];
$config_login_key_secret = $row['config_login_key_secret']; $config_login_key_secret = $row['config_login_key_secret'];
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']); // Azure / Entra for client
$azure_client_id = $row['config_azure_client_id'] ?? null;
// Login key verification $response = null;
// If no/incorrect 'key' is supplied, send to client portal instead $token_field = null;
if ($config_login_key_required) { $show_role_choice = false;
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { $email = '';
redirect("client"); $password = '';
}
}
// HTTP-Only cookies // Handle POST login request (normal login or role choice)
ini_set("session.cookie_httponly", true); if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']))) {
// Tell client to only send cookie(s) over HTTPS $email = sanitizeInput($_POST['email'] ?? '');
if ($config_https_only || !isset($config_https_only)) { $password = $_POST['password'] ?? '';
ini_set("session.cookie_secure", true); $role_choice = $_POST['role_choice'] ?? null; // 'agent' or 'client'
}
// Handle POST login request
if (isset($_POST['login'])) {
// Sessions should start after the user has POSTed data
session_start();
// Passed login brute force check
$email = sanitizeInput($_POST['email']);
$password = $_POST['password'];
$current_code = 0; // Default value
if (isset($_POST['current_code'])) {
$current_code = intval($_POST['current_code']);
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM users LEFT JOIN user_settings on users.user_id = user_settings.user_id WHERE user_email = '$email' AND user_archived_at IS NULL AND user_status = 1 AND user_type = 1"));
// Check password
if ($row && password_verify($password, $row['user_password'])) {
// User password correct (partial login)
// Set temporary user variables
$user_name = sanitizeInput($row['user_name']);
$user_id = intval($row['user_id']);
$session_user_id = $user_id; // to pass the user_id to logAction function
$user_email = sanitizeInput($row['user_email']);
$token = sanitizeInput($row['user_token']);
$force_mfa = intval($row['user_config_force_mfa']);
$user_role_id = intval($row['user_role_id']);
$user_encryption_ciphertext = $row['user_specific_encryption_ciphertext'];
$user_extension_key = $row['user_extension_key'];
$mfa_is_complete = false; // Default to requiring MFA
$extended_log = ''; // Default value
if (empty($token)) {
// MFA is not configured
$mfa_is_complete = true;
}
// Validate MFA via a remember-me cookie
if (isset($_COOKIE['rememberme'])) {
// Get remember tokens less than $config_login_remember_me_days_expire days old
$remember_tokens = mysqli_query($mysqli, "SELECT remember_token_token FROM remember_tokens WHERE remember_token_user_id = $user_id AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY)");
while ($row = mysqli_fetch_assoc($remember_tokens)) {
if (hash_equals($row['remember_token_token'], $_COOKIE['rememberme'])) {
$mfa_is_complete = true;
$extended_log = 'with 2FA remember-me cookie';
break;
}
}
}
// Validate MFA code
if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) {
$mfa_is_complete = true;
$extended_log = 'with MFA';
}
if ($mfa_is_complete) {
// MFA Completed successfully
// FULL LOGIN SUCCESS
// Create a remember me token, if requested
if (isset($_POST['remember_me'])) {
// TODO: Record the UA and IP a token is generated from so that can be shown later on
$newRememberToken = bin2hex(random_bytes(64));
setcookie('rememberme', $newRememberToken, time() + 86400*$config_login_remember_me_expire, "/", null, true, true);
mysqli_query($mysqli, "INSERT INTO remember_tokens SET remember_token_user_id = $user_id, remember_token_token = '$newRememberToken'");
$extended_log .= ", generated a new remember-me token";
}
// Check this login isn't suspicious
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(log_id) AS ip_previous_logins FROM logs WHERE log_type = 'Login' AND log_action = 'Success' AND log_ip = '$session_ip' AND log_user_id = $user_id"));
$ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']);
$sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(log_id) AS ua_previous_logins FROM logs WHERE log_type = 'Login' AND log_action = 'Success' AND log_user_agent = '$session_user_agent' AND log_user_id = $user_id"));
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
// Notify if both the user agent and IP are different
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
$subject = "$config_app_name new login for $user_name";
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [
[
'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name,
'recipient' => $user_email,
'recipient_name' => $user_name,
'subject' => $subject,
'body' => $body
]
];
addToMailQueue($data);
}
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
// Session info
$_SESSION['user_id'] = $user_id;
$_SESSION['csrf_token'] = randomString(156);
$_SESSION['logged'] = true;
// Forcing MFA
if ($force_mfa == 1 && $token == NULL) {
$config_start_page = "user/mfa_enforcement.php";
}
// Setup encryption session key
if (isset($user_encryption_ciphertext)) {
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
generateUserSessionKey($site_encryption_master_key);
// Setup extension - currently unused
//if (is_null($user_extension_key)) {
// Extension cookie
// Note: Browsers don't accept cookies with SameSite None if they are not HTTPS.
//setcookie("user_extension_key", "$user_extension_key", ['path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'None']);
// Set PHP session in DB, so we can access the session encryption data (above)
//$user_php_session = session_id();
//mysqli_query($mysqli, "UPDATE users SET user_php_session = '$user_php_session' WHERE user_id = $user_id");
//}
}
// Redirect to last visited or config home
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']) );
} else {
redirect("agent/$config_start_page");
}
} else {
// MFA is configured and needs to be confirmed, or was unsuccessful
// HTML code for the token input field
$token_field = "
<div class='input-group mb-3'>
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6' class='form-control' placeholder='Enter your 2FA code' name='current_code' required autofocus>
<div class='input-group-append'>
<div class='input-group-text'>
<span class='fas fa-key'></span>
</div>
</div>
</div>";
// Log/notify if MFA was unsuccessful
if ($current_code !== 0) {
// Logging
logAction("Login", "MFA Failed", "$user_name failed MFA", 0, $user_id);
// Email the tech to advise their credentials may be compromised
if (!empty($config_smtp_host)) {
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
$data = [
[
'from' => $config_mail_from_email,
'from_name' => $config_mail_from_name,
'recipient' => $user_email,
'recipient_name' => $user_name,
'subject' => $subject,
'body' => $body
]
];
$mail = addToMailQueue($data);
}
// HTML feedback for incorrect 2FA code
$response = "
<div class='alert alert-warning'>
Please Enter 2FA Code!
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
}
}
// Basic validation
if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
header("HTTP/1.1 401 Unauthorized");
$response = "
<div class='alert alert-danger'>
Incorrect username or password.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
} else { } else {
// Password incorrect or user doesn't exist - show generic error /*
* Unified lookup:
* - user_type = 1 Agent
* - user_type = 2 Client (must not be archived, client not archived)
* We fetch all possible matches for this email, then verify password per row.
* If both an agent and a client match with the same password:
* - First, show choice buttons (Agent / Client).
* - When user clicks a choice, we honor role_choice.
*/
$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)
)
");
header("HTTP/1.1 401 Unauthorized"); $agentRow = null;
$clientRow = null;
logAction("Login", "Failed", "Failed login attempt using $email"); while ($r = mysqli_fetch_assoc($sql)) {
if (!password_verify($password, $r['user_password'])) {
continue;
}
if (intval($r['user_type']) === 1 && $agentRow === null) {
$agentRow = $r;
}
if (intval($r['user_type']) === 2 && $clientRow === null) {
$clientRow = $r;
}
}
$response = " $selectedRow = null;
$selectedType = null; // 1 = agent, 2 = client
if ($agentRow === null && $clientRow === null) {
// No matching user/password combo
header("HTTP/1.1 401 Unauthorized");
logAction("Login", "Failed", "Failed login attempt using $email");
$response = "
<div class='alert alert-danger'> <div class='alert alert-danger'>
Incorrect username or password. Incorrect username or password.
<button class='close' data-dismiss='alert'>&times;</button> <button class='close' data-dismiss='alert'>&times;</button>
</div>"; </div>";
} elseif ($agentRow !== null && $clientRow !== null) {
// Both agent and client accounts share same email + password
if ($role_choice === 'agent') {
$selectedRow = $agentRow;
$selectedType = 1;
} elseif ($role_choice === 'client') {
$selectedRow = $clientRow;
$selectedType = 2;
} else {
// First time we realise this is a dual-role account: ask user to pick
$show_role_choice = true;
$response = "
<div class='alert alert-info'>
This login can be used as either an Agent account or a Client Portal account.
Please choose how you want to continue.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
}
} else {
// Only one valid row (agent OR client)
if ($agentRow !== null) {
$selectedRow = $agentRow;
$selectedType = 1;
} else {
$selectedRow = $clientRow;
$selectedType = 2;
}
}
// If we have a specific user selected, proceed with actual login
if ($selectedRow !== null && $selectedType !== null) {
$user_id = intval($selectedRow['user_id']);
$user_email = sanitizeInput($selectedRow['user_email']);
$session_user_id = $user_id; // to pass the user_id to logAction function
// =========================
// AGENT LOGIN FLOW
// =========================
if ($selectedType === 1) {
// Login key verification
// If no/incorrect 'key' is supplied, send to client portal instead
if ($config_login_key_required) {
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
redirect();
}
}
$user_name = sanitizeInput($selectedRow['user_name']);
$token = sanitizeInput($selectedRow['user_token']);
$force_mfa = intval($selectedRow['user_config_force_mfa']);
$user_role_id = intval($selectedRow['user_role_id']);
$user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext'];
$user_extension_key = $selectedRow['user_extension_key'];
$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 not configured
$mfa_is_complete = true;
}
// Validate MFA via a remember-me cookie
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) {
// FULL AGENT LOGIN SUCCESS
// Create a remember me token, if requested
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";
}
// 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 (!empty($user_encryption_ciphertext)) {
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
generateUserSessionKey($site_encryption_master_key);
}
// 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 {
// 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>";
if ($current_code !== 0) {
// Logging
logAction("Login", "MFA Failed", "$user_email 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
]
];
addToMailQueue($data);
}
$response = "
<div class='alert alert-warning'>
Please Enter 2FA Code!
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
}
}
// =========================
// CLIENT LOGIN FLOW
// =========================
} elseif ($selectedType === 2) {
if ($config_client_portal_enable != 1) {
// Client portal disabled
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.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
} else {
$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 {
// Not allowed or invalid
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.
<button class='close' data-dismiss='alert'>&times;</button>
</div>";
}
}
}
}
} }
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -313,10 +481,7 @@ if (isset($_POST['login'])) {
<!-- Font Awesome --> <!-- 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 -->
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 } ?>
@ -336,7 +501,6 @@ 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">
@ -350,16 +514,24 @@ if (isset($_POST['login'])) {
<form method="post"> <form method="post">
<div class="input-group mb-3" <?php if (isset($token_field)) { echo "hidden"; } ?>> <div class="input-group mb-3" <?php if (isset($token_field) && $token_field) { echo "hidden"; } ?>>
<input type="text" class="form-control" placeholder="Agent Email" name="email" value="<?php if (isset($token_field)) { echo $email; }?>" required <?php if (!isset($token_field)) { echo "autofocus"; } ?> > <input type="text" class="form-control"
placeholder="<?php if ($config_login_key_required) { if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { echo "Client "; } } echo "Email"; ?>"
name="email"
value="<?php echo htmlspecialchars($email ?? '', ENT_QUOTES); ?>"
required <?php if (!isset($token_field) || !$token_field) { echo "autofocus"; } ?>
>
<div class="input-group-append"> <div class="input-group-append">
<div class="input-group-text"> <div class="input-group-text">
<span class="fas fa-envelope"></span> <span class="fas fa-envelope"></span>
</div> </div>
</div> </div>
</div> </div>
<div class="input-group mb-3" <?php if (isset($token_field)) { echo "hidden"; } ?>>
<input type="password" class="form-control" placeholder="Agent Password" name="password" value="<?php if (isset($token_field)) { echo $password; } ?>" required> <div class="input-group mb-3" <?php if (isset($token_field) && $token_field) { echo "hidden"; } ?>>
<input type="password" class="form-control" placeholder="Password" name="password"
value="<?php echo isset($token_field) && $token_field ? htmlspecialchars($password ?? '', ENT_QUOTES) : ''; ?>"
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>
@ -368,38 +540,58 @@ if (isset($_POST['login'])) {
</div> </div>
<?php <?php
if (isset($token_field)) { // If agent needs MFA, show 2FA field + remember me
if (isset($token_field) && $token_field) {
echo $token_field; echo $token_field;
?> ?>
<div class="form-group mb-3"> <div class="form-group mb-3">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me"> <input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
<label class="custom-control-label" for="remember_me">Remember Me</label> <label class="custom-control-label" for="remember_me">Remember Me</label>
</div> </div>
</div> </div>
<?php
}
?>
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
<?php if($config_client_portal_enable == 1){ ?>
<hr>
<h5 class="text-center">Looking for the <a href="client">Client Portal?<a/></h5>
<?php } ?> <?php } ?>
<?php if ($show_role_choice): ?>
<!-- When both agent & client accounts exist with same email/password -->
<div class="mb-2 text-center">
<button type="submit" class="btn btn-primary btn-block mb-2" name="role_choice" value="agent">
Log in as Agent
</button>
<button type="submit" class="btn btn-success btn-block" name="role_choice" value="client">
Log in as Client
</button>
</div>
<?php else: ?>
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
<?php endif; ?>
</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 -->
<?php
if (!$config_whitelabel_enabled) {
echo '<small class="text-muted">Powered by ITFlow</small>';
}
?>
<!-- jQuery --> <!-- jQuery -->
<script src="plugins/jquery/jquery.min.js"></script> <script src="plugins/jquery/jquery.min.js"></script>
@ -410,8 +602,6 @@ if (isset($_POST['login'])) {
<!-- AdminLTE App --> <!-- 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 --> <!-- Prevents resubmit on refresh or back -->
<script src="js/login_prevent_resubmit.js"></script> <script src="js/login_prevent_resubmit.js"></script>