Files
projectsend/routes/settings.php
T
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00

192 lines
15 KiB
PHP

<?php
use App\Http\Controllers\Settings\PasswordController;
use App\Http\Controllers\Settings\ProfileController;
use App\Modules\Clients\Http\Controllers\ClientSettingsController;
use App\Modules\Comments\Http\Controllers\CommentSettingsController;
use App\Modules\Files\Http\Controllers\FileRetentionSettingsController;
use App\Modules\Files\Http\Controllers\UploadSettingsController;
use App\Modules\Identity\Http\Controllers\ApiTokensController;
use App\Modules\Identity\Http\Controllers\ConnectedAccountsController;
use App\Modules\Identity\Http\Controllers\LdapSettingsController;
use App\Modules\Identity\Http\Controllers\SecuritySettingsController;
use App\Modules\Identity\Http\Controllers\SocialLoginController;
use App\Modules\Identity\Http\Controllers\SocialLoginSettingsController;
use App\Modules\Identity\Http\Controllers\TwoFactorEnrollmentController;
use App\Modules\Notifications\Http\Controllers\NotificationPreferencesController;
use App\Modules\Platform\Http\Controllers\AboutController;
use App\Modules\Platform\Http\Controllers\CaptchaSettingsController;
use App\Modules\Platform\Http\Controllers\EmailSettingsController;
use App\Modules\Platform\Http\Controllers\EmailTemplatesController;
use App\Modules\Platform\Http\Controllers\ExternalStorageSettingsController;
use App\Modules\Platform\Http\Controllers\LanguageSettingsController;
use App\Modules\Platform\Http\Controllers\PrivacySettingsController;
use App\Modules\Platform\Http\Controllers\PublicListingSettingsController;
use App\Modules\Platform\Http\Controllers\SchedulerMonitoringController;
use App\Modules\Platform\Http\Controllers\SystemSettingsController;
use App\Modules\Platform\Http\Controllers\ThemingSettingsController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function () {
// Route::redirect()'s destination needs a leading slash, or Laravel
// deliberately emits a *relative* Location header (stripping the
// leading slash it would otherwise generate) instead of an absolute
// one — see Illuminate\Routing\RedirectController. A relative
// redirect resolves against the current path's directory, which
// breaks the moment the redirect lives more than one segment deep
// (e.g. system/settings -> system/settings/general would resolve to
// system/system/settings/general). Always use an absolute path here.
Route::redirect('settings', '/settings/profile');
Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::get('settings/delete-account', [ProfileController::class, 'deleteAccount'])->name('profile.delete-account');
Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::get('settings/password', [PasswordController::class, 'edit'])->name('password.edit');
Route::put('settings/password', [PasswordController::class, 'update'])->name('password.update');
Route::get('settings/two-factor', [TwoFactorEnrollmentController::class, 'show'])->name('two-factor.show');
// Staff and clients alike: which providers you have connected is a
// property of your account, not of your role.
Route::get('settings/connected-accounts', [ConnectedAccountsController::class, 'edit'])
->name('connected-accounts.edit');
Route::post('settings/connected-accounts/{provider}', [SocialLoginController::class, 'connect'])
->middleware('throttle:20,1,social-connect')
->name('connected-accounts.connect');
Route::delete('settings/connected-accounts/{provider}', [ConnectedAccountsController::class, 'destroy'])
->name('connected-accounts.destroy');
// Changing the state of the second factor re-proves the first one. A
// stolen session is exactly the situation 2FA exists to survive, so
// "disable 2FA" must not be reachable with nothing but that session.
// `confirm` is deliberately outside this group: it is mid-enrollment,
// already proves possession of the TOTP secret, and the enrollment it
// completes was itself password-confirmed by `store`.
Route::middleware('password.confirm')->group(function () {
Route::post('settings/two-factor', [TwoFactorEnrollmentController::class, 'store'])->name('two-factor.enable');
Route::post('settings/two-factor/recovery-codes', [TwoFactorEnrollmentController::class, 'regenerateRecoveryCodes'])
->name('two-factor.recovery-codes');
Route::delete('settings/two-factor', [TwoFactorEnrollmentController::class, 'destroy'])->name('two-factor.disable');
});
// Named bucket — see the note in routes/auth.php. A bare `throttle:`
// shares one counter with every other bare one, so confirming an
// enrolment code used to draw on the same six as re-sending a
// verification email.
Route::post('settings/two-factor/confirm', [TwoFactorEnrollmentController::class, 'confirm'])
->middleware('throttle:6,1,two-factor-confirm')->name('two-factor.confirm');
// API tokens are account-level credentials, so they live in the personal
// settings section — but staff-only, because /api/v1 is staff-only.
// Creating and revoking re-prove the password for the same reason the
// two-factor block above does: a token outlives the session that minted
// it, so a stolen session must not be enough to mint one.
Route::middleware('staff')->group(function () {
// `create` before `{token}`, or the literal segment is swallowed by
// the id parameter — the same ordering rule as files/orphans in
// routes/web.php.
Route::get('settings/api-tokens/create', [ApiTokensController::class, 'create'])->name('api-tokens.create');
Route::get('settings/api-tokens', [ApiTokensController::class, 'index'])->name('api-tokens.index');
Route::get('settings/api-tokens/{token}/edit', [ApiTokensController::class, 'edit'])->name('api-tokens.edit');
Route::middleware('password.confirm')->group(function () {
Route::post('settings/api-tokens', [ApiTokensController::class, 'store'])->name('api-tokens.store');
// Editing can widen what an already-issued secret may do, so it
// re-proves the password exactly as minting does.
Route::patch('settings/api-tokens/{token}', [ApiTokensController::class, 'update'])->name('api-tokens.update');
Route::delete('settings/api-tokens/{token}', [ApiTokensController::class, 'destroy'])->name('api-tokens.destroy');
});
});
Route::get('settings/notifications', [NotificationPreferencesController::class, 'edit'])->name('notification-preferences.edit');
Route::put('settings/notifications', [NotificationPreferencesController::class, 'update'])->name('notification-preferences.update');
// Read-only, so `staff` alone rather than the `can:edit_settings`
// group below: the release you are running, its licence and where
// its source lives are not a configuration secret, and the sidebar
// footer links every staff member here regardless of what they may
// edit.
Route::middleware('staff')->get('system/about', AboutController::class)->name('system.about');
// System-wide configuration — deliberately outside the account
// settings section; it lives under its own sidebar entry, one route
// per section (general today; clients, email, … as modules land).
Route::middleware(['staff', 'can:edit_settings'])->group(function () {
Route::redirect('system/settings', '/system/settings/general');
Route::get('system/settings/general', [SystemSettingsController::class, 'edit'])->name('system-settings.edit');
Route::patch('system/settings/general', [SystemSettingsController::class, 'update'])->name('system-settings.update');
Route::get('system/settings/security', [SecuritySettingsController::class, 'edit'])->name('system-settings.security.edit');
Route::patch('system/settings/security', [SecuritySettingsController::class, 'update'])->name('system-settings.security.update');
Route::get('system/settings/clients', [ClientSettingsController::class, 'edit'])->name('system-settings.clients.edit');
Route::patch('system/settings/clients', [ClientSettingsController::class, 'update'])->name('system-settings.clients.update');
Route::get('system/settings/uploads', [UploadSettingsController::class, 'edit'])->name('system-settings.uploads.edit');
Route::patch('system/settings/uploads', [UploadSettingsController::class, 'update'])->name('system-settings.uploads.update');
Route::get('system/settings/file-retention', [FileRetentionSettingsController::class, 'edit'])->name('system-settings.file-retention.edit');
Route::patch('system/settings/file-retention', [FileRetentionSettingsController::class, 'update'])->name('system-settings.file-retention.update');
Route::get('system/settings/comments', [CommentSettingsController::class, 'edit'])->name('system-settings.comments.edit');
Route::patch('system/settings/comments', [CommentSettingsController::class, 'update'])->name('system-settings.comments.update');
Route::get('system/settings/email', [EmailSettingsController::class, 'edit'])->name('system-settings.email.edit');
Route::patch('system/settings/email', [EmailSettingsController::class, 'update'])->name('system-settings.email.update');
Route::post('system/settings/email/test', [EmailSettingsController::class, 'sendTest'])->name('system-settings.email.test');
// Deliberately outside any capability: group. LDAP is an
// administrator's setting, available in every edition, not an
// edition difference.
Route::get('system/settings/ldap', [LdapSettingsController::class, 'edit'])->name('system-settings.ldap.edit');
Route::patch('system/settings/ldap', [LdapSettingsController::class, 'update'])->name('system-settings.ldap.update');
Route::post('system/settings/ldap/test', [LdapSettingsController::class, 'test'])->name('system-settings.ldap.test');
// Outside any capability: group for the same reason as LDAP above.
Route::get('system/settings/social-login', [SocialLoginSettingsController::class, 'edit'])
->name('system-settings.social-login.edit');
Route::patch('system/settings/social-login/{provider}', [SocialLoginSettingsController::class, 'update'])
->name('system-settings.social-login.update');
// Outside any capability: group for the same reason as LDAP above.
// Only the option of using the platform's own keys is an edition
// difference, and that is gated per field inside the controller.
Route::get('system/settings/captcha', [CaptchaSettingsController::class, 'edit'])->name('system-settings.captcha.edit');
Route::patch('system/settings/captcha', [CaptchaSettingsController::class, 'update'])->name('system-settings.captcha.update');
Route::post('system/settings/captcha/test', [CaptchaSettingsController::class, 'test'])->name('system-settings.captcha.test');
Route::get('system/settings/privacy', [PrivacySettingsController::class, 'edit'])->name('system-settings.privacy.edit');
Route::patch('system/settings/privacy', [PrivacySettingsController::class, 'update'])->name('system-settings.privacy.update');
Route::get('system/settings/public-listing', [PublicListingSettingsController::class, 'edit'])->name('system-settings.public-listing.edit');
Route::patch('system/settings/public-listing', [PublicListingSettingsController::class, 'update'])->name('system-settings.public-listing.update');
Route::get('system/settings/languages', [LanguageSettingsController::class, 'edit'])->name('system-settings.languages.edit');
Route::patch('system/settings/languages', [LanguageSettingsController::class, 'update'])->name('system-settings.languages.update');
Route::get('system/settings/theming', [ThemingSettingsController::class, 'edit'])->name('system-settings.theming.edit');
Route::patch('system/settings/theming', [ThemingSettingsController::class, 'update'])->name('system-settings.theming.update');
Route::get('system/settings/theming/email-preview/{key}', [ThemingSettingsController::class, 'previewEmail'])
->name('system-settings.theming.email-preview');
// Every field on this page is Community-only (Capability::StorageConfigure)
// — unlike email settings, there's no capability-independent
// sub-field to keep serving in Cloud, so the whole surface is
// gated at the route level instead of per-field in the controller.
Route::middleware('capability:storage.configure')->group(function () {
Route::get('system/settings/storage', [ExternalStorageSettingsController::class, 'edit'])->name('system-settings.storage.edit');
Route::patch('system/settings/storage', [ExternalStorageSettingsController::class, 'update'])->name('system-settings.storage.update');
Route::post('system/settings/storage/test', [ExternalStorageSettingsController::class, 'testConnection'])->name('system-settings.storage.test');
});
// Same all-or-nothing shape as Storage above (Capability::SchedulerMonitoring).
Route::middleware('capability:scheduler.monitoring')->group(function () {
Route::get('system/settings/scheduler', [SchedulerMonitoringController::class, 'index'])->name('system-settings.scheduler.index');
Route::post('system/settings/scheduler/failed-jobs/{uuid}/retry', [SchedulerMonitoringController::class, 'retryFailedJob'])->name('system-settings.scheduler.retry');
Route::delete('system/settings/scheduler/failed-jobs/{uuid}', [SchedulerMonitoringController::class, 'destroyFailedJob'])->name('system-settings.scheduler.destroy');
Route::delete('system/settings/scheduler/failed-jobs', [SchedulerMonitoringController::class, 'destroyAllFailedJobs'])->name('system-settings.scheduler.destroy-all');
});
});
// Distinct from edit_settings, matching v1's exact permission split.
Route::middleware(['staff', 'can:edit_email_templates'])->group(function () {
Route::get('system/settings/email-templates', [EmailTemplatesController::class, 'index'])->name('email-templates.index');
Route::get('system/settings/email-templates/{slot}', [EmailTemplatesController::class, 'edit'])->name('email-templates.edit');
Route::patch('system/settings/email-templates/{slot}', [EmailTemplatesController::class, 'update'])->name('email-templates.update');
Route::delete('system/settings/email-templates/{slot}', [EmailTemplatesController::class, 'destroy'])->name('email-templates.destroy');
Route::get('system/settings/email-templates/{slot}/preview', [EmailTemplatesController::class, 'preview'])->name('email-templates.preview');
});
});