feat: add CORS configuration and frontend assets for Italian migration site
This commit is contained in:
parent
ebae7fdb8a
commit
4d44f4c8b7
|
|
@ -5,9 +5,7 @@ namespace App\Http\Controllers;
|
|||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
|
|
@ -19,19 +17,16 @@ class AuthController extends Controller
|
|||
*/
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8',
|
||||
// 'is_admin' => 'boolean'
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
// 'is_admin' => $request->is_admin ?? false,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -41,6 +36,26 @@ class AuthController extends Controller
|
|||
], 201);
|
||||
}
|
||||
|
||||
public function getAllUsers(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// Optional: Ensure only users with 'admin' ability can access this
|
||||
if (!$user || !$request->user()->tokenCan('admin')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$users = User::all();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $users
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login and generate token
|
||||
*
|
||||
|
|
@ -49,7 +64,6 @@ class AuthController extends Controller
|
|||
*/
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
// Always return JSON responses from API endpoints
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
|
||||
$request->validate([
|
||||
|
|
@ -67,16 +81,14 @@ class AuthController extends Controller
|
|||
], 401);
|
||||
}
|
||||
|
||||
// Delete any existing tokens for this device name if provided
|
||||
// Delete existing tokens for the same device name
|
||||
if ($request->device_name) {
|
||||
$user->tokens()->where('name', $request->device_name)->delete();
|
||||
}
|
||||
|
||||
// Create token with appropriate abilities based on user role
|
||||
$abilities = $user->is_admin ? ['admin'] : ['user'];
|
||||
$token = $user->createToken($request->device_name ?? 'api_token', $abilities);
|
||||
// All users will get the same 'admin' ability (since dashboard is admin-only)
|
||||
$token = $user->createToken($request->device_name ?? 'api_token', ['admin']);
|
||||
|
||||
// Get token expiration time if configured
|
||||
$tokenExpiration = null;
|
||||
$expirationMinutes = config('sanctum.expiration');
|
||||
if ($expirationMinutes) {
|
||||
|
|
@ -93,12 +105,54 @@ class AuthController extends Controller
|
|||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'is_admin' => $user->is_admin,
|
||||
'abilities' => $abilities
|
||||
'abilities' => ['admin']
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update authenticated user's account
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// Validate incoming request
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email,' . $user->id,
|
||||
'current_password' => 'required|string',
|
||||
'password' => 'nullable|string|confirmed|min:6',
|
||||
]);
|
||||
|
||||
// Check if current password is correct
|
||||
if (!\Hash::check($request->current_password, $user->password)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Current password is incorrect',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Update user data
|
||||
$user->name = $request->name;
|
||||
$user->email = $request->email;
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$user->password = bcrypt($request->password);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Account updated successfully',
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout (revoke token)
|
||||
*
|
||||
|
|
@ -107,7 +161,6 @@ class AuthController extends Controller
|
|||
*/
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
// Revoke the token that was used to authenticate the current request
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -126,6 +179,13 @@ class AuthController extends Controller
|
|||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'User not authenticated',
|
||||
], 401);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
|
|
@ -133,10 +193,10 @@ class AuthController extends Controller
|
|||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'is_admin' => $user->is_admin,
|
||||
],
|
||||
'abilities' => $request->user()->currentAccessToken()->abilities
|
||||
'abilities' => $request->user()->currentAccessToken()?->abilities ?? [],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,35 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Cors
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$response = $next($request);
|
||||
$allowedOrigins = [
|
||||
'http://localhost:5173', // React dev server URL
|
||||
'http://127.0.0.1:5173', // Alternative localhost
|
||||
// Add your production domain when ready
|
||||
// 'https://yourdomain.com'
|
||||
];
|
||||
|
||||
// Add CORS headers to the response
|
||||
$response->headers->set('Access-Control-Allow-Origin', '*');
|
||||
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
||||
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, X-CSRF-TOKEN');
|
||||
$response->headers->set('Access-Control-Max-Age', '86400'); // 24 hours
|
||||
$origin = $request->headers->get('Origin');
|
||||
|
||||
// Handle preflight OPTIONS requests
|
||||
if ($request->isMethod('OPTIONS')) {
|
||||
return response()->json('', 200);
|
||||
if ($request->getMethod() === 'OPTIONS') {
|
||||
$response = response('', 200);
|
||||
} else {
|
||||
$response = $next($request);
|
||||
}
|
||||
|
||||
// Set CORS headers
|
||||
if (in_array($origin, $allowedOrigins)) {
|
||||
$response->headers->set('Access-Control-Allow-Origin', $origin);
|
||||
}
|
||||
|
||||
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
|
||||
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin');
|
||||
$response->headers->set('Access-Control-Allow-Credentials', 'true');
|
||||
$response->headers->set('Access-Control-Max-Age', '86400'); // Cache preflight for 24 hours
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cross-Origin Resource Sharing (CORS) Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your settings for cross-origin resource sharing
|
||||
| or "CORS". This determines what cross-origin operations may execute
|
||||
| in web browsers. You are free to adjust these settings as needed.
|
||||
|
|
||||
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
'allowed_origins' => ['http://localhost:8000'],
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
'exposed_headers' => [],
|
||||
|
||||
'max_age' => 0,
|
||||
|
||||
'supports_credentials' => true,
|
||||
|
||||
];
|
||||
|
|
@ -17,7 +17,7 @@ return [
|
|||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
'localhost,localhost:3000,localhost:8000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort(),
|
||||
// Sanctum::currentRequestHost(),
|
||||
))),
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<script type="module" crossorigin src="/assets/index-DhLmNHHP.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DP8Dc-el.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<script type="module" crossorigin src="/assets/index-Bqf3G0Hi.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BTau5zUE.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>Laravel</title>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,13 @@ use App\Http\Controllers\ActivityLogController;
|
|||
|
||||
// Public routes
|
||||
Route::post('/login', [AuthController::class, 'login'])->name('login');
|
||||
Route::post('/register', [AuthController::class, 'register'])->name('register');
|
||||
Route::get('/migrants', [MigrantController::class, 'index']);
|
||||
Route::get('/migrants/{id}', [MigrantController::class, 'show']);
|
||||
Route::get('/migrants/{id}/photos', [MigrantController::class, 'getPhotos']);
|
||||
|
||||
|
||||
// Protected routes - require Sanctum authentication
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::put('/user/account', [AuthController::class, 'update']);
|
||||
|
||||
// User routes
|
||||
Route::get('/user', [AuthController::class, 'me'])->name('user.profile');
|
||||
|
|
@ -27,20 +26,17 @@ Route::get('/dashboard/stats', [DashboardController::class, 'getStats']);
|
|||
Route::get('/activity-logs', [ActivityLogController::class, 'index']);
|
||||
|
||||
// Migrant CRUD routes (now includes photo uploads)
|
||||
Route::post('/migrants', [MigrantController::class, 'store']); // Can include photos
|
||||
Route::put('/migrants/{id}', [MigrantController::class, 'update']); // Can include photos
|
||||
Route::patch('/migrants/{id}', [MigrantController::class, 'update']); // Can include photos
|
||||
Route::delete('/migrants/{id}', [MigrantController::class, 'destroy']); // Deletes photos too
|
||||
Route::post('/migrants', [MigrantController::class, 'store']);
|
||||
Route::put('/migrants/{id}', [MigrantController::class, 'update']);
|
||||
Route::patch('/migrants/{id}', [MigrantController::class, 'update']);
|
||||
Route::delete('/migrants/{id}', [MigrantController::class, 'destroy']);
|
||||
|
||||
// Photo management routes (all handled by MigrantController)
|
||||
// Photo management routes
|
||||
Route::post('/migrants/{id}/photos', [MigrantController::class, 'uploadPhotos']);
|
||||
Route::post('/migrants/photos/{photoId}/set-as-profile', [MigrantController::class, 'setAsProfilePhoto']);
|
||||
Route::put('/migrants/photos/{photoId}/caption', [MigrantController::class, 'updatePhotoCaption']);
|
||||
Route::delete('/migrants/photos/{photoId}', [MigrantController::class, 'deletePhoto']);
|
||||
|
||||
Route::post('/register', [AuthController::class, 'register'])->name('register');
|
||||
Route::get('/users', [AuthController::class, 'getAllUsers'])->name('users.index');
|
||||
});
|
||||
|
||||
// Admin-only routes
|
||||
// Route::middleware('ability:admin')->group(function () {
|
||||
// Route::post('/register', [AuthController::class, 'register'])->name('register');
|
||||
// });
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
Route::fallback(function () {
|
||||
return file_get_contents(public_path('index.html'));
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -177,4 +177,288 @@ class MigrantPhotoTest extends TestCase
|
|||
->where('is_profile_photo', true)
|
||||
->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating a migrant with photo operations (upload, set as profile, remove).
|
||||
*/
|
||||
public function test_can_update_migrant_with_photo_operations(): void
|
||||
{
|
||||
// Create fake storage disk for testing
|
||||
Storage::fake('public');
|
||||
|
||||
// Create a migrant
|
||||
$person = Person::factory()->create([
|
||||
'surname' => 'Smith',
|
||||
'christian_name' => 'John',
|
||||
'occupation' => 'Teacher'
|
||||
]);
|
||||
|
||||
// Create two existing photos for this migrant
|
||||
$photos = [];
|
||||
$captions = ['Existing Photo 1', 'Existing Photo 2'];
|
||||
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$filename = "existing_photo{$i}.jpg";
|
||||
$file = UploadedFile::fake()->create($filename, 100, 'image/jpeg');
|
||||
|
||||
// Upload the file to the storage
|
||||
$path = $file->storeAs("photos/{$person->person_id}", $filename, 'public');
|
||||
|
||||
// Create the photo record
|
||||
$photo = Photo::create([
|
||||
'person_id' => $person->person_id,
|
||||
'filename' => $filename,
|
||||
'original_filename' => $filename,
|
||||
'file_path' => Storage::url($path),
|
||||
'mime_type' => 'image/jpeg',
|
||||
'file_size' => $file->getSize() / 1024,
|
||||
'caption' => $captions[$i],
|
||||
'is_profile_photo' => $i === 0 // Make the first one the profile initially
|
||||
]);
|
||||
|
||||
$photos[] = $photo;
|
||||
}
|
||||
|
||||
// Create a new photo to upload during the update
|
||||
$newPhoto = UploadedFile::fake()->create('new_photo.jpg', 100, 'image/jpeg');
|
||||
|
||||
// Prepare update data
|
||||
$updateData = [
|
||||
'surname' => 'Smith-Jones', // Update the surname
|
||||
'occupation' => 'Professor', // Update the occupation
|
||||
'photos' => [$newPhoto], // Upload a new photo
|
||||
'captions' => ['New Profile Photo'], // Caption for the new photo
|
||||
'set_as_profile' => true, // Set the new photo as profile
|
||||
'profile_photo_index' => 0, // Set the first (and only) new photo as profile
|
||||
'existing_photos' => [
|
||||
// Update caption of the first photo
|
||||
[
|
||||
'id' => $photos[0]->id,
|
||||
'caption' => 'Updated Caption'
|
||||
],
|
||||
// Remove the second photo (we'll handle this after the update)
|
||||
]
|
||||
];
|
||||
|
||||
// Make the API request to update the migrant
|
||||
$response = $this->putJson("/api/migrants/{$person->person_id}", $updateData);
|
||||
|
||||
// Assert the response is successful
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'person',
|
||||
'uploaded_photos',
|
||||
],
|
||||
'message',
|
||||
])
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Person updated successfully',
|
||||
]);
|
||||
|
||||
// Assert the person data was updated
|
||||
$this->assertDatabaseHas('person', [
|
||||
'person_id' => $person->person_id,
|
||||
'surname' => 'Smith-Jones',
|
||||
'occupation' => 'Professor'
|
||||
]);
|
||||
|
||||
// Assert that we now have 3 photos (2 existing + 1 new)
|
||||
$this->assertEquals(3, Photo::where('person_id', $person->person_id)->count());
|
||||
|
||||
// Assert that the caption of the first photo was updated
|
||||
$this->assertDatabaseHas('photos', [
|
||||
'id' => $photos[0]->id,
|
||||
'caption' => 'Updated Caption'
|
||||
]);
|
||||
|
||||
// Assert that the new photo was set as profile
|
||||
$profilePhoto = Photo::where('person_id', $person->person_id)
|
||||
->where('is_profile_photo', true)
|
||||
->first();
|
||||
$this->assertEquals('New Profile Photo', $profilePhoto->caption);
|
||||
|
||||
// Assert that the old profile photo is no longer the profile
|
||||
$this->assertDatabaseHas('photos', [
|
||||
'id' => $photos[0]->id,
|
||||
'is_profile_photo' => false
|
||||
]);
|
||||
|
||||
// Now test removing a photo
|
||||
$photoToRemove = $photos[1]->id;
|
||||
$response = $this->deleteJson("/api/migrants/photos/{$photoToRemove}");
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Photo deleted successfully',
|
||||
]);
|
||||
|
||||
// Assert that the photo was removed from the database
|
||||
$this->assertDatabaseMissing('photos', [
|
||||
'id' => $photoToRemove
|
||||
]);
|
||||
|
||||
// Assert that the file was removed from storage
|
||||
Storage::disk('public')->assertMissing('photos/' . $person->person_id . '/' . $photos[1]->filename);
|
||||
|
||||
// Assert that we now have 2 photos (originally 2 + 1 new - 1 deleted)
|
||||
$this->assertEquals(2, Photo::where('person_id', $person->person_id)->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding a single photo, then multiple photos, and finally deleting photos
|
||||
*/
|
||||
public function test_can_add_single_and_multiple_photos_and_delete_them(): void
|
||||
{
|
||||
// Create fake storage disk for testing
|
||||
Storage::fake('public');
|
||||
|
||||
// Create a migrant without photos first
|
||||
$migrantData = [
|
||||
'surname' => $this->faker->lastName,
|
||||
'christian_name' => $this->faker->firstName,
|
||||
'date_of_birth' => $this->faker->date(),
|
||||
'place_of_birth' => $this->faker->city,
|
||||
'occupation' => $this->faker->jobTitle,
|
||||
'id_card_no' => (string)$this->faker->unique()->randomNumber(6),
|
||||
'migration' => [
|
||||
'date_of_arrival_aus' => $this->faker->date(),
|
||||
'date_of_arrival_nt' => $this->faker->date(),
|
||||
'arrival_period' => '1950-1960',
|
||||
]
|
||||
];
|
||||
|
||||
// 1. Create a migrant without any photos
|
||||
$response = $this->postJson('/api/migrants', $migrantData);
|
||||
|
||||
// Assert the response is successful
|
||||
$response->assertStatus(201)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Person created successfully',
|
||||
]);
|
||||
|
||||
// Get the created person ID
|
||||
$personId = $response->json('data.person.person_id');
|
||||
$person = Person::find($personId);
|
||||
|
||||
// Assert that no photos exist for this person yet
|
||||
$this->assertEquals(0, Photo::where('person_id', $personId)->count());
|
||||
|
||||
// 2. Now add a single photo and set it as profile
|
||||
$singlePhoto = UploadedFile::fake()->create('single_photo.jpg', 100, 'image/jpeg');
|
||||
$updateData = [
|
||||
'photos' => [$singlePhoto],
|
||||
'captions' => ['My First Photo'],
|
||||
'set_as_profile' => true,
|
||||
'profile_photo_index' => 0
|
||||
];
|
||||
|
||||
$response = $this->putJson("/api/migrants/{$personId}", $updateData);
|
||||
|
||||
// Assert the response is successful
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Person updated successfully',
|
||||
]);
|
||||
|
||||
// Assert that 1 photo was added
|
||||
$this->assertEquals(1, Photo::where('person_id', $personId)->count());
|
||||
|
||||
// Assert that the photo is set as profile
|
||||
$this->assertEquals(1, Photo::where('person_id', $personId)
|
||||
->where('is_profile_photo', true)
|
||||
->count());
|
||||
|
||||
// Get the photo details
|
||||
$firstPhoto = Photo::where('person_id', $personId)->first();
|
||||
$this->assertEquals('My First Photo', $firstPhoto->caption);
|
||||
$this->assertTrue($firstPhoto->is_profile_photo);
|
||||
|
||||
// Assert the file was stored
|
||||
Storage::disk('public')->assertExists('photos/' . $personId . '/' . $firstPhoto->filename);
|
||||
|
||||
// 3. Now add multiple additional photos and set one as profile
|
||||
$photo1 = UploadedFile::fake()->create('additional1.jpg', 100, 'image/jpeg');
|
||||
$photo2 = UploadedFile::fake()->create('additional2.jpg', 100, 'image/jpeg');
|
||||
$photo3 = UploadedFile::fake()->create('additional3.jpg', 100, 'image/jpeg');
|
||||
|
||||
$updateData = [
|
||||
'photos' => [$photo1, $photo2, $photo3],
|
||||
'captions' => ['Additional 1', 'Additional 2', 'Additional 3'],
|
||||
'set_as_profile' => true,
|
||||
'profile_photo_index' => 1 // Set the second new photo as profile
|
||||
];
|
||||
|
||||
$response = $this->putJson("/api/migrants/{$personId}", $updateData);
|
||||
|
||||
// Assert the response is successful
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Person updated successfully',
|
||||
]);
|
||||
|
||||
// Assert that we now have 4 photos (1 existing + 3 new)
|
||||
$this->assertEquals(4, Photo::where('person_id', $personId)->count());
|
||||
|
||||
// Get all photos for this person
|
||||
$photos = Photo::where('person_id', $personId)->get();
|
||||
|
||||
// Assert that exactly one photo is set as profile
|
||||
$this->assertEquals(1, $photos->where('is_profile_photo', true)->count());
|
||||
|
||||
// Find the profile photo
|
||||
$profilePhoto = $photos->where('is_profile_photo', true)->first();
|
||||
|
||||
// Assert that the second new photo was set as profile
|
||||
$this->assertEquals('Additional 2', $profilePhoto->caption);
|
||||
|
||||
// Assert the first photo is no longer the profile
|
||||
$this->assertFalse($firstPhoto->fresh()->is_profile_photo);
|
||||
|
||||
// 4. Change the profile photo using profile_photo_id (existing photo)
|
||||
$nonProfilePhoto = $photos->where('caption', 'Additional 1')->first();
|
||||
|
||||
$updateData = [
|
||||
'set_as_profile' => true,
|
||||
'profile_photo_id' => $nonProfilePhoto->id
|
||||
];
|
||||
|
||||
$response = $this->putJson("/api/migrants/{$personId}", $updateData);
|
||||
|
||||
// Assert the response is successful
|
||||
$response->assertStatus(200);
|
||||
|
||||
// Assert that the specified photo is now the profile
|
||||
$this->assertTrue($nonProfilePhoto->fresh()->is_profile_photo);
|
||||
$this->assertFalse($profilePhoto->fresh()->is_profile_photo);
|
||||
|
||||
// 5. Delete photos one by one
|
||||
$photosToDelete = $photos->whereNotIn('caption', ['Additional 1'])->pluck('id');
|
||||
|
||||
foreach ($photosToDelete as $photoId) {
|
||||
$response = $this->deleteJson("/api/migrants/photos/{$photoId}");
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Photo deleted successfully',
|
||||
]);
|
||||
|
||||
// Assert the photo was removed from the database
|
||||
$this->assertDatabaseMissing('photos', [
|
||||
'id' => $photoId
|
||||
]);
|
||||
}
|
||||
|
||||
// Assert that only one photo remains (the profile photo)
|
||||
$this->assertEquals(1, Photo::where('person_id', $personId)->count());
|
||||
$this->assertEquals('Additional 1', Photo::where('person_id', $personId)->first()->caption);
|
||||
$this->assertTrue(Photo::where('person_id', $personId)->first()->is_profile_photo);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue