feat: add import restore options, OAuth confirmation, and fixes

- database import: add keep_owners (restore PostgreSQL owners and
  privileges) and sqlite_database (pick the target SQLite file) to the
  API, OpenAPI spec, and import form
- oauth: require password confirmation and support team selection on
  OAuth login
- remote process: send team broadcast events to the team of the server
  that ran the process instead of the causer's user id
- servers API: handle the legacy build server setting
- compose: support parent bind paths and volume driver options
- dns: improve record hints and provider management
- notifications: fix throttling and missing backup notifications
- ui: rework the confirmation modal
- add feature and unit tests for the new behavior
This commit is contained in:
Andras Bacsai
2026-09-30 13:35:22 +02:00
parent d1bdecba6d
commit 9e7cb60b0d
57 changed files with 3032 additions and 194 deletions
+98 -1
View File
@@ -4,11 +4,15 @@ namespace App\Http\Controllers;
use App\Models\OauthSetting;
use App\Services\Auth\OauthLoginService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\HttpException;
class OauthController extends Controller
{
public const string PASSWORD_CONFIRMATION_SESSION_KEY = 'oauth.password_confirmation';
public function redirect(string $provider)
{
$oauthSetting = $this->enabledProvider($provider);
@@ -17,8 +21,40 @@ class OauthController extends Controller
return $socialiteProvider->redirect();
}
public function callback(string $provider, OauthLoginService $oauthLoginService)
/**
* Start confirming a destructive action by re-authenticating the logged-in
* user through an OAuth provider the user has a linked identity with.
*/
public function confirm(Request $request, string $provider)
{
$user = $request->user();
$oauthSetting = $this->enabledProvider($provider);
if (! $user->oauthIdentities()->where('provider', $oauthSetting->provider)->exists()) {
abort(403, 'No identity from this provider is linked to your account.');
}
$request->session()->put(self::PASSWORD_CONFIRMATION_SESSION_KEY, [
'user_id' => $user->getKey(),
'provider' => $oauthSetting->provider,
'return_to' => $this->sameOriginUrl(url()->previous()),
]);
$socialiteProvider = get_socialite_provider($oauthSetting->provider);
$parameters = socialite_reauthentication_parameters($oauthSetting);
if ($parameters !== []) {
$socialiteProvider->with($parameters);
}
return $socialiteProvider->redirect();
}
public function callback(Request $request, string $provider, OauthLoginService $oauthLoginService)
{
$pendingConfirmation = $request->session()->pull(self::PASSWORD_CONFIRMATION_SESSION_KEY);
if (is_array($pendingConfirmation) && $request->user()) {
return $this->confirmCallback($request, $provider, $pendingConfirmation, $oauthLoginService);
}
try {
$oauthSetting = $this->enabledProvider($provider);
$oauthUser = get_socialite_provider($oauthSetting->provider)->user();
@@ -38,6 +74,67 @@ class OauthController extends Controller
}
}
/**
* Complete an OAuth confirmation. The returned identity must already be
* linked to the logged-in user; the session is never switched to another
* user and no identity is linked or updated.
*
* @param array{user_id?: mixed, provider?: mixed, return_to?: mixed} $pendingConfirmation
*/
private function confirmCallback(Request $request, string $provider, array $pendingConfirmation, OauthLoginService $oauthLoginService): RedirectResponse
{
$user = $request->user();
try {
$oauthSetting = $this->enabledProvider($provider);
if (
data_get($pendingConfirmation, 'user_id') !== $user->getKey()
|| data_get($pendingConfirmation, 'provider') !== $oauthSetting->provider
) {
throw new HttpException(403, 'OAuth confirmation does not match the current session');
}
$oauthUser = get_socialite_provider($oauthSetting->provider)->user();
if (! $oauthLoginService->identityBelongsToUser($user, $oauthSetting, $oauthUser)) {
throw new HttpException(403, 'OAuth identity is not linked to the current user');
}
} catch (\Exception $e) {
auditLog('auth.oauth.password_confirmation_failed', [
'provider' => $provider,
'actor_id' => $user->getKey(),
'actor_email' => $user->email,
'exception_class' => $e::class,
], 'warning');
Log::warning('OAuth password confirmation failed.', [
'provider' => $provider,
'user_id' => $user->getKey(),
'exception_class' => $e::class,
'exception_message' => $e->getMessage(),
]);
abort(403, 'Confirmation failed. Sign in with the account linked to your Coolify user.');
}
$request->session()->passwordConfirmed();
auditLog('auth.oauth.password_confirmed', [
'provider' => $oauthSetting->provider,
'actor_id' => $user->getKey(),
'actor_email' => $user->email,
]);
return redirect()->to($this->sameOriginUrl(data_get($pendingConfirmation, 'return_to')));
}
private function sameOriginUrl(mixed $url): string
{
$home = url('/');
if (! is_string($url) || ($url !== $home && ! str_starts_with($url, $home.'/') && ! str_starts_with($url, $home.'?'))) {
return $home;
}
return $url;
}
private function logCallbackFailure(string $provider, \Throwable $exception): void
{
auditLog('auth.oauth.callback_failed', [