Merge commit '7e675e7beb25a8ddcb3f6cc6f0101f0ffd668e1e' into HEAD
This commit is contained in:
@@ -331,11 +331,36 @@ class TicketingSettingsController extends Controller
|
|||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$emailConnections = $isBootstrap
|
// #6168 — the Email tab's rows. Shaped by hand rather than handed over
|
||||||
|
// as models for one reason: `config` holds the mailbox PASSWORD, and
|
||||||
|
// this prop ships on every settings load, so a credential left in it is
|
||||||
|
// a credential in the page source of everyone who can open Help Desk
|
||||||
|
// Settings. The form asks for a new password instead and
|
||||||
|
// updateEmailConnection() keeps the stored one when the box is blank,
|
||||||
|
// so nothing needs it on the client — `has_password` is all the UI has
|
||||||
|
// to know to say "a password is set".
|
||||||
|
$emailConnections = ($isBootstrap
|
||||||
? collect()
|
? collect()
|
||||||
: EmailConnection::when($visibleGroupIds !== null,
|
: EmailConnection::when($visibleGroupIds !== null,
|
||||||
fn($q) => $q->whereIn('group_id', $visibleGroupIds))
|
fn($q) => $q->whereIn('group_id', $visibleGroupIds))
|
||||||
->get();
|
->orderBy('id')
|
||||||
|
->get())
|
||||||
|
->map(function (EmailConnection $connection) {
|
||||||
|
$config = $connection->config ?? [];
|
||||||
|
$hasPassword = ($config['password'] ?? '') !== '';
|
||||||
|
unset($config['password']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $connection->id,
|
||||||
|
'group_id' => $connection->group_id,
|
||||||
|
'type' => $connection->type,
|
||||||
|
'active' => (bool) $connection->active,
|
||||||
|
'config' => $config,
|
||||||
|
'has_password' => $hasPassword,
|
||||||
|
'last_polled_at' => $connection->last_polled_at?->toIso8601String(),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
|
||||||
return Inertia::render('Ticketing/Settings', [
|
return Inertia::render('Ticketing/Settings', [
|
||||||
'activeTab' => $tab,
|
'activeTab' => $tab,
|
||||||
@@ -913,12 +938,43 @@ class TicketingSettingsController extends Controller
|
|||||||
$connection->update([
|
$connection->update([
|
||||||
'type' => $request->type,
|
'type' => $request->type,
|
||||||
'active' => $request->boolean('active', true),
|
'active' => $request->boolean('active', true),
|
||||||
'config' => $request->input('config', $connection->config),
|
'config' => $this->mergedConnectionConfig($request, $connection),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return back()->with('success', 'Email connection updated.');
|
return back()->with('success', 'Email connection updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The config to store for an edited connection.
|
||||||
|
*
|
||||||
|
* #6168 — the page is never sent the stored password (see index()), so the
|
||||||
|
* edit form's password box arrives EMPTY on every save that does not change
|
||||||
|
* it. Taking the request's config as-is would therefore erase the mailbox
|
||||||
|
* credential every time somebody renamed a host or flipped the Active
|
||||||
|
* checkbox. A blank box means "keep the one you have"; typing a new one
|
||||||
|
* replaces it, and the rest of the config is the request's, so a field
|
||||||
|
* cleared on purpose still clears.
|
||||||
|
*/
|
||||||
|
private function mergedConnectionConfig(Request $request, EmailConnection $connection): array
|
||||||
|
{
|
||||||
|
$config = $request->input('config');
|
||||||
|
$existing = $connection->config ?? [];
|
||||||
|
|
||||||
|
if (! is_array($config)) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($config['password'] ?? '') === '') {
|
||||||
|
unset($config['password']);
|
||||||
|
|
||||||
|
if (($existing['password'] ?? null) !== null) {
|
||||||
|
$config['password'] = $existing['password'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
public function destroyEmailConnection(EmailConnection $connection)
|
public function destroyEmailConnection(EmailConnection $connection)
|
||||||
{
|
{
|
||||||
$this->requireAgentAccess();
|
$this->requireAgentAccess();
|
||||||
|
|||||||
@@ -41,8 +41,15 @@ Route::middleware(['web', 'auth', 'app.access:ticketing'])
|
|||||||
// ── Settings (must be before /{ticket} wildcard) ──────────────────────
|
// ── Settings (must be before /{ticket} wildcard) ──────────────────────
|
||||||
Route::middleware('permission:ticketing.settings')->group(function () {
|
Route::middleware('permission:ticketing.settings')->group(function () {
|
||||||
Route::get('/settings', [TicketingSettingsController::class, 'index']) ->name('settings');
|
Route::get('/settings', [TicketingSettingsController::class, 'index']) ->name('settings');
|
||||||
|
// #6168 — `email` belongs in this list. It was the one tab segment
|
||||||
|
// the whitelist omitted while the page shipped an `emailConnections`
|
||||||
|
// prop on every render and the three write routes below were live, so
|
||||||
|
// the only deep link an admin could guess fell through to the POST-only
|
||||||
|
// `/settings/email` and answered 405. The Vue's TAB_KEYS and this list
|
||||||
|
// are now pinned to each other by
|
||||||
|
// tests/Unit/TicketingSettingsTabsAreOneVocabularyTest.php.
|
||||||
Route::get('/settings/{tab}', [TicketingSettingsController::class, 'index'])
|
Route::get('/settings/{tab}', [TicketingSettingsController::class, 'index'])
|
||||||
->where('tab', 'groups|agents|priorities|statuses|projects|automation')->name('settings.tab');
|
->where('tab', 'groups|agents|priorities|statuses|projects|email|automation')->name('settings.tab');
|
||||||
Route::post('/settings/groups', [TicketingSettingsController::class, 'storeGroup']) ->name('settings.groups.store');
|
Route::post('/settings/groups', [TicketingSettingsController::class, 'storeGroup']) ->name('settings.groups.store');
|
||||||
Route::put('/settings/groups/{group}', [TicketingSettingsController::class, 'updateGroup']) ->name('settings.groups.update');
|
Route::put('/settings/groups/{group}', [TicketingSettingsController::class, 'updateGroup']) ->name('settings.groups.update');
|
||||||
Route::delete('/settings/groups/{group}', [TicketingSettingsController::class, 'destroyGroup']) ->name('settings.groups.destroy');
|
Route::delete('/settings/groups/{group}', [TicketingSettingsController::class, 'destroyGroup']) ->name('settings.groups.destroy');
|
||||||
|
|||||||
Reference in New Issue
Block a user