Adds messages when stuff goes wrong

This commit is contained in:
2026-03-19 12:21:58 +01:00
parent a373b60750
commit dbafa6c99c
4 changed files with 156 additions and 29 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -8,6 +8,7 @@
use App\Models\Role; use App\Models\Role;
use App\Models\User; use App\Models\User;
use App\Services\ActivityLogger; use App\Services\ActivityLogger;
use Illuminate\Database\QueryException;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@@ -100,41 +101,47 @@ private function processCallback(): RedirectResponse
Log::info('[Azure SSO] Full Azure user dump', json_decode(json_encode($azureUser), true)); Log::info('[Azure SSO] Full Azure user dump', json_decode(json_encode($azureUser), true));
$user = User::query()->updateOrCreate( try {
['username' => $azureUser->getEmail()], $user = User::query()->updateOrCreate(
[ ['username' => $azureUser->getEmail()],
'name' => $azureUser->getName(), [
'email' => $azureUser->user['mail'] ?? $azureUser->getEmail(), 'name' => $azureUser->getName(),
'azure_id' => $azureUser->getId(), 'email' => $azureUser->user['mail'] ?? $azureUser->getEmail(),
'photo' => $azureUser->getAvatar(), 'azure_id' => $azureUser->getId(),
'job_title' => Arr::get($azureUser->user, 'jobTitle'), 'photo' => $azureUser->getAvatar(),
'department' => Arr::get($azureUser->user, 'department'), 'job_title' => Arr::get($azureUser->user, 'jobTitle'),
'company_name' => Arr::get($azureUser->user, 'companyName'), 'department' => Arr::get($azureUser->user, 'department'),
'phone' => Arr::get($azureUser->user, 'mobilePhone', Arr::get($azureUser->user, 'businessPhones.0')), 'company_name' => Arr::get($azureUser->user, 'companyName'),
'email_verified_at' => now(), 'phone' => Arr::get($azureUser->user, 'mobilePhone', Arr::get($azureUser->user, 'businessPhones.0')),
] 'email_verified_at' => now(),
); ]
);
Log::info('[Azure SSO] Local user upserted', [ Log::info('[Azure SSO] Local user upserted', [
'user_id' => $user->id,
'email' => $user->email,
'was_recent' => $user->wasRecentlyCreated,
'role_id' => $user->role_id,
]);
if ($user->role_id === null) {
$user->update(['role_id' => Role::where('name', 'user')->first()->id]);
Log::info('[Azure SSO] Default role assigned', [
'user_id' => $user->id, 'user_id' => $user->id,
'email' => $user->email,
'was_recent' => $user->wasRecentlyCreated,
'role_id' => $user->role_id, 'role_id' => $user->role_id,
]); ]);
if ($user->role_id === null) {
$user->update(['role_id' => Role::where('name', 'user')->first()->id]);
Log::info('[Azure SSO] Default role assigned', [
'user_id' => $user->id,
'role_id' => $user->role_id,
]);
}
auth()->login($user);
ActivityLogger::log('login', $user->id, metadata: ['email' => $user->email, 'firm_name' => Arr::get($azureUser->user, 'companyName')]);
} catch (QueryException $e) {
Log::error('[Azure SSO] Database error during user upsert', ['message' => $e->getMessage(), 'email' => $azureUser->getEmail()]);
return redirect('/')->with('error', 'Something went wrong during sign-in. Please try again or contact support.');
} }
auth()->login($user);
ActivityLogger::log('login', $user->id, metadata: ['email' => $user->email, 'firm_name' => Arr::get($azureUser->user, 'companyName')]);
return redirect('/'); return redirect('/');
} }
} }

View File

@@ -0,0 +1,118 @@
<script setup>
import { ref, watch, onUnmounted } from 'vue'
import { usePage } from '@inertiajs/vue3'
import { CheckCircleIcon, ExclamationTriangleIcon, XMarkIcon } from '@heroicons/vue/20/solid'
const page = usePage()
const visible = ref(false)
const message = ref('')
const type = ref('success') // 'success' | 'error'
let autoDismissTimer = null
function clearTimer() {
if (autoDismissTimer) {
clearTimeout(autoDismissTimer)
autoDismissTimer = null
}
}
function dismiss() {
visible.value = false
clearTimer()
}
function showFlash(flash) {
clearTimer()
if (flash?.error) {
message.value = flash.error
type.value = 'error'
visible.value = true
// Error messages stay until manually dismissed
} else if (flash?.success) {
message.value = flash.success
type.value = 'success'
visible.value = true
// Auto-dismiss success after 8 seconds
autoDismissTimer = setTimeout(() => {
visible.value = false
}, 8000)
} else {
visible.value = false
}
}
// Watch for flash prop changes on each Inertia page visit
watch(
() => page.props.flash,
(flash) => showFlash(flash),
{ immediate: true, deep: true }
)
onUnmounted(() => {
clearTimer()
})
</script>
<template>
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="-translate-y-full opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="-translate-y-full opacity-0"
>
<div
v-if="visible"
:class="[
'relative z-40 w-full shadow-lg',
type === 'error'
? 'bg-rose-600'
: 'bg-emerald-600',
]"
role="alert"
:aria-live="type === 'error' ? 'assertive' : 'polite'"
>
<div class="px-6 py-3 flex items-center gap-3 max-w-7xl mx-auto">
<!-- Icon -->
<component
:is="type === 'error' ? ExclamationTriangleIcon : CheckCircleIcon"
class="w-5 h-5 text-white/90 shrink-0"
aria-hidden="true"
/>
<!-- Message -->
<p class="flex-1 text-sm font-medium text-white leading-snug">
{{ message }}
</p>
<!-- Dismiss button -->
<button
type="button"
class="ml-auto shrink-0 rounded p-1 text-white/70 hover:text-white hover:bg-white/10 transition-colors focus:outline-none focus:ring-2 focus:ring-white/40"
aria-label="Dismiss notification"
@click="dismiss"
>
<XMarkIcon class="w-4 h-4" />
</button>
</div>
<!-- Progress bar for success auto-dismiss -->
<div
v-if="type === 'success'"
class="absolute bottom-0 left-0 h-[2px] bg-white/30 animate-[shrink_8s_linear_forwards]"
style="width: 100%"
/>
</div>
</Transition>
</template>
<style scoped>
@keyframes shrink {
from { width: 100%; }
to { width: 0%; }
}
</style>

View File

@@ -2,6 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3' import { usePage } from '@inertiajs/vue3'
import PageHeader from '@/Components/PageHeader.vue' import PageHeader from '@/Components/PageHeader.vue'
import FlashNotification from '@/Components/FlashNotification.vue'
const page = usePage() const page = usePage()
@@ -13,6 +14,7 @@ const pageTitle = computed(() => {
<template> <template>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
<PageHeader :title="pageTitle" /> <PageHeader :title="pageTitle" />
<FlashNotification />
<!-- Growth symbol watermark --> <!-- Growth symbol watermark -->
<img <img