From 07e2796e09b09350bbfd75ceb04a68c89b3a2c76 Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 21 Jun 2026 16:53:39 +1000 Subject: [PATCH] Added API --- app/Http/Controllers/UserFlightController.php | 15 ------- app/Http/Middleware/HandleInertiaRequests.php | 1 + .../Middleware/SanctumOrTrustedOrigin.php | 4 +- app/Listeners/IssueApiToken.php | 23 ++++++++++ app/Listeners/RevokeApiToken.php | 23 ++++++++++ app/Providers/AppServiceProvider.php | 4 +- bootstrap/app.php | 2 + resources/js/Composables/useApiResource.ts | 42 +++++++++++++++++++ resources/js/Composables/useFlights.ts | 23 ++++------ resources/js/Types/types.d.ts | 1 + resources/js/api.ts | 17 ++++++++ routes/api.php | 5 ++- 12 files changed, 124 insertions(+), 36 deletions(-) delete mode 100644 app/Http/Controllers/UserFlightController.php create mode 100644 app/Listeners/IssueApiToken.php create mode 100644 app/Listeners/RevokeApiToken.php create mode 100644 resources/js/Composables/useApiResource.ts create mode 100644 resources/js/api.ts diff --git a/app/Http/Controllers/UserFlightController.php b/app/Http/Controllers/UserFlightController.php deleted file mode 100644 index a63c546..0000000 --- a/app/Http/Controllers/UserFlightController.php +++ /dev/null @@ -1,15 +0,0 @@ - $request->user(), 'roles' => $request->user()?->getRoleNames() ?? [], 'permissions' => $request->user()?->getAllPermissions()->pluck('name') ?? [], + 'apiToken' => session('api_token'), ], 'achievement_notifications' => fn() => $request->user() ? $request->user() diff --git a/app/Http/Middleware/SanctumOrTrustedOrigin.php b/app/Http/Middleware/SanctumOrTrustedOrigin.php index 0125990..1409ced 100644 --- a/app/Http/Middleware/SanctumOrTrustedOrigin.php +++ b/app/Http/Middleware/SanctumOrTrustedOrigin.php @@ -4,6 +4,7 @@ namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpFoundation\Response; class SanctumOrTrustedOrigin @@ -11,7 +12,8 @@ class SanctumOrTrustedOrigin public function handle(Request $request, Closure $next): Response { // Authenticated via Sanctum (cookie or token) — let it through, auth()->user() is set. - if ($request->user('sanctum')) { + if ($user = $request->user('sanctum')) { + Auth::setUser($user); return $next($request); } diff --git a/app/Listeners/IssueApiToken.php b/app/Listeners/IssueApiToken.php new file mode 100644 index 0000000..8303795 --- /dev/null +++ b/app/Listeners/IssueApiToken.php @@ -0,0 +1,23 @@ +user; + $user->tokens()->where('name', 'frontend')->delete(); + $token = $user->createToken( + 'frontend', + ['*'], + now()->addHours(4) + )->plainTextToken; + + session(['api_token' => $token]); + } +} diff --git a/app/Listeners/RevokeApiToken.php b/app/Listeners/RevokeApiToken.php new file mode 100644 index 0000000..144dff7 --- /dev/null +++ b/app/Listeners/RevokeApiToken.php @@ -0,0 +1,23 @@ +user; + + $user?->tokens()->where('name', 'frontend')->delete(); + session()->forget('api_token'); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2f79f57..95d5393 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -31,8 +31,8 @@ class AppServiceProvider extends ServiceProvider UserFlight::observe(FlightObserver::class); Airline::observe(AirlineObserver::class); RateLimiter::for('api', function (Request $request) { - return $request->user() - ? Limit::perMinute(60)->by($request->user()->id) + return $request->user('sanctum') + ? Limit::perMinute(60)->by($request->user('sanctum')->id) : Limit::perMinute(10)->by($request->ip()); }); diff --git a/bootstrap/app.php b/bootstrap/app.php index 335b908..4bcb295 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,6 +1,7 @@ RoleMiddleware::class, 'permission' => PermissionMiddleware::class, 'role_or_permission' => RoleOrPermissionMiddleware::class, + 'sanctum.or.trusted' => SanctumOrTrustedOrigin::class, ]); // }) diff --git a/resources/js/Composables/useApiResource.ts b/resources/js/Composables/useApiResource.ts new file mode 100644 index 0000000..b83dde9 --- /dev/null +++ b/resources/js/Composables/useApiResource.ts @@ -0,0 +1,42 @@ +import { ref, onMounted, onUnmounted, type Ref } from 'vue' +import axios from 'axios' +import { api } from '@/api' + +interface UseApiResourceReturn { + data: Ref + loading: Ref + error: Ref + refresh: () => Promise +} + +export function useApiResource(url: string, immediate = true): UseApiResourceReturn { + const data = ref(null) as Ref + const loading = ref(true) + const error = ref(null) + + let controller = new AbortController() + + async function refresh() { + controller.abort() + controller = new AbortController() + loading.value = true + error.value = null + + try { + const response = await api.get(url, { signal: controller.signal }) + data.value = response.data + } catch (e) { + if (!axios.isCancel(e)) { + error.value = 'Failed to load data' + console.error(e) + } + } finally { + loading.value = false + } + } + + if (immediate) onMounted(refresh) + onUnmounted(() => controller.abort()) + + return { data, loading, error, refresh } +} diff --git a/resources/js/Composables/useFlights.ts b/resources/js/Composables/useFlights.ts index 6a2cca5..8b7929c 100644 --- a/resources/js/Composables/useFlights.ts +++ b/resources/js/Composables/useFlights.ts @@ -1,21 +1,12 @@ -// useFlights.ts -import {onMounted, ref} from "vue"; -import {Flight} from "@/Types/types"; -import axios from "axios"; +import { computed } from 'vue' +import { useApiResource } from '@/Composables/useApiResource' +import type { Flight } from '@/Types/types' export function useFlights(url: string, departedOnly: boolean = false) { - const flights = ref([]) - const flightsLoading = ref(true) + const requestUrl = departedOnly ? `${url}/departed` : url + const { data, loading, error } = useApiResource(requestUrl) - onMounted(async () => { - try { - const requestUrl = departedOnly ? `${url}/departed` : url - const response = await axios.get(requestUrl) - flights.value = response.data - } finally { - flightsLoading.value = false - } - }) + const flights = computed(() => data.value ?? []) - return { flights, flightsLoading } + return { flights, flightsLoading: loading, error } } diff --git a/resources/js/Types/types.d.ts b/resources/js/Types/types.d.ts index 9880851..f1a7bbb 100644 --- a/resources/js/Types/types.d.ts +++ b/resources/js/Types/types.d.ts @@ -97,6 +97,7 @@ export type SharedProps = import('@inertiajs/core').PageProps & { isLoggedIn: boolean roles: string[]; permissions: string[]; + apiToken: string | null; }, logo_api_url: string achievement_notifications: Notification[] diff --git a/resources/js/api.ts b/resources/js/api.ts new file mode 100644 index 0000000..8284219 --- /dev/null +++ b/resources/js/api.ts @@ -0,0 +1,17 @@ +import axios from 'axios' +import { usePage } from '@inertiajs/vue3' +import {SharedProps} from "@/Types/types"; + +export const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL, + withCredentials: true, + headers: { Accept: 'application/json' }, +}) + +api.interceptors.request.use((config) => { + const token = usePage().props.auth?.apiToken + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) diff --git a/routes/api.php b/routes/api.php index 59f835b..b42ae54 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,9 +12,10 @@ Route::domain(config('app.api_domain'))->group(function () { return response()->json(['message' => 'Welcome to the FlightsGoneBy API']); }); - Route::prefix('user')->controller(UserApiController::class)->group(function () { + Route::prefix('user')->controller(UserApiController::class)->middleware('sanctum.or.trusted')->group(function () { Route::get('{user}/flights', 'viewableFlights')->name('api.user.flights'); - Route::get('{user}/flights/departed', 'viewableDepartedFlights')->name('api.user.flights'); + Route::get('{user}/flights/departed', 'viewableDepartedFlights')->name('api.user.flights.departed'); + Route::get('{user}/flights/upcoming', 'viewableUpcomingFlights')->name('api.user.flights.departed'); });