Initial commit
This commit is contained in:
36
api/app/Actions/Events/CreateEventAction.php
Normal file
36
api/app/Actions/Events/CreateEventAction.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Events;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateEventAction
|
||||
{
|
||||
public function execute(array $data, User $user): Event
|
||||
{
|
||||
return \Illuminate\Support\Facades\DB::transaction(function () use ($data, $user) {
|
||||
$slug = $this->generateUniqueSlug($data['slug'] ?? Str::slug($data['name']));
|
||||
$data['slug'] = $slug;
|
||||
$data['user_id'] = $user->id;
|
||||
$event = new Event($data);
|
||||
$event->save();
|
||||
|
||||
return $event;
|
||||
});
|
||||
}
|
||||
|
||||
protected function generateUniqueSlug(string $base): string
|
||||
{
|
||||
$slug = Str::slug($base);
|
||||
$original = $slug;
|
||||
$count = 0;
|
||||
while (Event::where('slug', $slug)->exists()) {
|
||||
$count++;
|
||||
$slug = $original.'-'.$count;
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
34
api/app/Actions/Events/UpdateEventAction.php
Normal file
34
api/app/Actions/Events/UpdateEventAction.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Events;
|
||||
|
||||
use App\Models\Event;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UpdateEventAction
|
||||
{
|
||||
public function execute(Event $event, array $data): Event
|
||||
{
|
||||
return \Illuminate\Support\Facades\DB::transaction(function () use ($event, $data) {
|
||||
if (isset($data['slug']) && $data['slug'] !== $event->slug) {
|
||||
$data['slug'] = $this->generateUniqueSlug($data['slug'], $event->id);
|
||||
}
|
||||
$event->update($data);
|
||||
|
||||
return $event->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
protected function generateUniqueSlug(string $base, int $excludeId): string
|
||||
{
|
||||
$slug = Str::slug($base);
|
||||
$original = $slug;
|
||||
$count = 0;
|
||||
while (Event::where('slug', $slug)->where('id', '!=', $excludeId)->exists()) {
|
||||
$count++;
|
||||
$slug = $original.'-'.$count;
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
44
api/app/Console/Commands/MakeAdminUser.php
Normal file
44
api/app/Console/Commands/MakeAdminUser.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MakeAdminUser extends Command
|
||||
{
|
||||
protected $signature = 'make:admin
|
||||
{--email= : Admin email address}
|
||||
{--password= : Admin password}';
|
||||
|
||||
protected $description = 'Create an admin user (email and password required)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$email = $this->option('email');
|
||||
$password = $this->option('password');
|
||||
|
||||
if (! $email || ! $password) {
|
||||
$this->error('Please provide --email and --password.');
|
||||
$this->line('Example: php artisan make:admin --email=admin@example.com --password=secret');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (User::where('email', $email)->exists()) {
|
||||
$this->error("A user with email {$email} already exists.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
User::create([
|
||||
'name' => explode('@', $email)[0],
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->info("Admin user created for {$email}.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
71
api/app/Http/Controllers/Admin/EventsController.php
Normal file
71
api/app/Http/Controllers/Admin/EventsController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Actions\Events\CreateEventAction;
|
||||
use App\Actions\Events\UpdateEventAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\StoreEventRequest;
|
||||
use App\Http\Requests\Admin\UpdateEventRequest;
|
||||
use App\Models\Event;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EventsController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$events = Event::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->withCount('uploads')
|
||||
->orderByDesc('created_at')
|
||||
->paginate($request->input('per_page', 15));
|
||||
|
||||
return response()->json($events);
|
||||
}
|
||||
|
||||
public function store(StoreEventRequest $request, CreateEventAction $action): JsonResponse
|
||||
{
|
||||
$event = $action->execute($request->validated(), $request->user());
|
||||
|
||||
return response()->json($event->loadCount('uploads'), 201);
|
||||
}
|
||||
|
||||
public function show(Event $event, Request $request): JsonResponse
|
||||
{
|
||||
$this->authorizeEvent($event, $request);
|
||||
|
||||
return response()->json($event->loadCount('uploads'));
|
||||
}
|
||||
|
||||
public function update(UpdateEventRequest $request, Event $event, UpdateEventAction $action): JsonResponse
|
||||
{
|
||||
$this->authorizeEvent($event, $request);
|
||||
$event = $action->execute($event, $request->validated());
|
||||
|
||||
return response()->json($event->loadCount('uploads'));
|
||||
}
|
||||
|
||||
public function destroy(Event $event, Request $request): JsonResponse
|
||||
{
|
||||
$this->authorizeEvent($event, $request);
|
||||
$event->delete();
|
||||
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
|
||||
public function uploads(Event $event, Request $request): JsonResponse
|
||||
{
|
||||
$this->authorizeEvent($event, $request);
|
||||
$uploads = $event->uploads()->orderByDesc('created_at')->paginate($request->input('per_page', 20));
|
||||
|
||||
return response()->json($uploads);
|
||||
}
|
||||
|
||||
protected function authorizeEvent(Event $event, Request $request): void
|
||||
{
|
||||
if ($event->user_id !== $request->user()->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
api/app/Http/Controllers/Admin/GoogleDriveController.php
Normal file
103
api/app/Http/Controllers/Admin/GoogleDriveController.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\GoogleDrive\GoogleDriveService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GoogleDriveController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected GoogleDriveService $googleDrive
|
||||
) {}
|
||||
|
||||
public function authUrl(): JsonResponse
|
||||
{
|
||||
$url = $this->googleDrive->getAuthUrl();
|
||||
|
||||
return response()->json(['url' => $url]);
|
||||
}
|
||||
|
||||
public function callback(Request $request): RedirectResponse
|
||||
{
|
||||
$frontendUrl = config('app.frontend_admin_url', 'http://localhost:5173');
|
||||
|
||||
$code = $request->query('code');
|
||||
if (! $code) {
|
||||
return redirect($frontendUrl.'?google_drive_error=missing_code');
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
if (! $user) {
|
||||
return redirect($frontendUrl.'?google_drive_error=not_authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->googleDrive->handleCallback($code, $user);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Google Drive connection failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
return redirect($frontendUrl.'?google_drive_error=connection_failed');
|
||||
}
|
||||
|
||||
return redirect($frontendUrl.'?google_drive_connected=1');
|
||||
}
|
||||
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$connection = Auth::user()?->googleDriveConnections()->first();
|
||||
|
||||
return response()->json([
|
||||
'connected' => (bool) $connection,
|
||||
'account_email' => $connection?->account_email,
|
||||
]);
|
||||
}
|
||||
|
||||
public function disconnect(): JsonResponse
|
||||
{
|
||||
Auth::user()?->googleDriveConnections()->delete();
|
||||
|
||||
return response()->json(['message' => 'Disconnected']);
|
||||
}
|
||||
|
||||
public function sharedDrives(): JsonResponse
|
||||
{
|
||||
$drives = $this->googleDrive->listSharedDrives(Auth::user());
|
||||
|
||||
return response()->json(['data' => $drives]);
|
||||
}
|
||||
|
||||
public function folders(Request $request): JsonResponse
|
||||
{
|
||||
$parentId = $request->query('parent_id');
|
||||
$driveId = $request->query('drive_id');
|
||||
$folders = $this->googleDrive->listFolders(Auth::user(), $parentId, $driveId);
|
||||
|
||||
return response()->json(['data' => $folders]);
|
||||
}
|
||||
|
||||
public function createFolder(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'parent_id' => ['nullable', 'string'],
|
||||
'drive_id' => ['nullable', 'string'],
|
||||
]);
|
||||
$folder = $this->googleDrive->createFolder(
|
||||
Auth::user(),
|
||||
$request->input('name'),
|
||||
$request->input('parent_id'),
|
||||
$request->input('drive_id')
|
||||
);
|
||||
|
||||
return response()->json(['data' => $folder]);
|
||||
}
|
||||
}
|
||||
55
api/app/Http/Controllers/Admin/UploadsController.php
Normal file
55
api/app/Http/Controllers/Admin/UploadsController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Upload;
|
||||
use App\Services\GoogleDrive\GoogleDriveService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UploadsController extends Controller
|
||||
{
|
||||
public function show(Upload $upload, Request $request): JsonResponse
|
||||
{
|
||||
$this->authorizeUpload($upload, $request);
|
||||
|
||||
return response()->json($upload);
|
||||
}
|
||||
|
||||
public function destroy(Upload $upload, Request $request, GoogleDriveService $googleDrive): JsonResponse
|
||||
{
|
||||
$this->authorizeUpload($upload, $request);
|
||||
|
||||
if ($upload->google_drive_file_id) {
|
||||
try {
|
||||
$googleDrive->deleteFile($request->user(), $upload->google_drive_file_id);
|
||||
} catch (\Throwable) {
|
||||
// Continue to delete record even if Drive delete fails
|
||||
}
|
||||
}
|
||||
$upload->delete();
|
||||
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
|
||||
public function downloadUrl(Upload $upload, Request $request, GoogleDriveService $googleDrive): JsonResponse
|
||||
{
|
||||
$this->authorizeUpload($upload, $request);
|
||||
|
||||
if (! $upload->google_drive_file_id) {
|
||||
return response()->json(['message' => 'File not yet available'], 404);
|
||||
}
|
||||
|
||||
$url = $googleDrive->getFileLink($request->user(), $upload->google_drive_file_id);
|
||||
|
||||
return response()->json(['url' => $url]);
|
||||
}
|
||||
|
||||
protected function authorizeUpload(Upload $upload, Request $request): void
|
||||
{
|
||||
if ($upload->event->user_id !== $request->user()->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
api/app/Http/Controllers/AuthController.php
Normal file
47
api/app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
'remember' => ['boolean'],
|
||||
]);
|
||||
|
||||
if (! Auth::attempt(
|
||||
$request->only('email', 'password'),
|
||||
$request->boolean('remember')
|
||||
)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [__('auth.failed')],
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return response()->json(['user' => Auth::user()]);
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return response()->json(['message' => 'Logged out']);
|
||||
}
|
||||
|
||||
public function user(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json($request->user());
|
||||
}
|
||||
}
|
||||
8
api/app/Http/Controllers/Controller.php
Normal file
8
api/app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
183
api/app/Http/Controllers/Public/EventUploadController.php
Normal file
183
api/app/Http/Controllers/Public/EventUploadController.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Public;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Public\VerifyPasswordRequest;
|
||||
use App\Jobs\ProcessEventUpload;
|
||||
use App\Models\Event;
|
||||
use App\Models\Upload;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EventUploadController extends Controller
|
||||
{
|
||||
public function show(string $slug): JsonResponse
|
||||
{
|
||||
$event = Event::where('slug', $slug)->where('is_active', true)->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'name' => $event->name,
|
||||
'description' => $event->description,
|
||||
'slug' => $event->slug,
|
||||
'is_active' => $event->is_active,
|
||||
'upload_start_at' => $event->upload_start_at?->toIso8601String(),
|
||||
'upload_end_at' => $event->upload_end_at?->toIso8601String(),
|
||||
'max_file_size_mb' => $event->max_file_size_mb,
|
||||
'allowed_extensions' => $event->allowed_extensions,
|
||||
'require_password' => $event->require_password,
|
||||
'has_password' => $event->has_password,
|
||||
]);
|
||||
}
|
||||
|
||||
public function verifyPassword(VerifyPasswordRequest $request, string $slug): JsonResponse
|
||||
{
|
||||
$event = Event::where('slug', $slug)->where('is_active', true)->firstOrFail();
|
||||
|
||||
if (! $event->require_password) {
|
||||
return response()->json(['verified' => true]);
|
||||
}
|
||||
|
||||
if (! Hash::check($request->password, $event->upload_password)) {
|
||||
RateLimiter::hit('password-verify:'.$request->ip());
|
||||
|
||||
return response()->json(['message' => 'Invalid password'], 401);
|
||||
}
|
||||
|
||||
return response()->json(['verified' => true]);
|
||||
}
|
||||
|
||||
public function upload(Request $request, string $slug): JsonResponse
|
||||
{
|
||||
\Log::info('Upload request received', [
|
||||
'slug' => $slug,
|
||||
'has_file' => $request->hasFile('file'),
|
||||
'files' => array_keys($request->allFiles()),
|
||||
'content_length' => $request->header('Content-Length'),
|
||||
]);
|
||||
|
||||
$event = Event::where('slug', $slug)->where('is_active', true)->firstOrFail();
|
||||
|
||||
if ($event->require_password) {
|
||||
$password = $request->header('X-Upload-Password');
|
||||
if (! $password || ! Hash::check($password, $event->upload_password)) {
|
||||
return response()->json(['message' => 'Invalid or missing upload password'], 401);
|
||||
}
|
||||
}
|
||||
|
||||
$this->validateUploadWindow($event);
|
||||
|
||||
$request->validate([
|
||||
'file' => ['required', 'file'],
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
|
||||
\Log::info('File details', [
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'size' => $file->getSize(),
|
||||
'mime' => $file->getMimeType(),
|
||||
'is_valid' => $file->isValid(),
|
||||
'error' => $file->getError(),
|
||||
'temp_path' => $file->getPathname(),
|
||||
]);
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$extension = strtolower($file->getClientOriginalExtension() ?: $file->guessExtension());
|
||||
|
||||
if (! in_array($extension, $event->allowed_extensions ?? [], true)) {
|
||||
return response()->json([
|
||||
'message' => 'File type not allowed. Allowed: '.implode(', ', $event->allowed_extensions),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$maxBytes = $event->max_file_size_mb * 1024 * 1024;
|
||||
if ($file->getSize() > $maxBytes) {
|
||||
return response()->json([
|
||||
'message' => 'File too large. Maximum size: '.$event->max_file_size_mb.' MB',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Ensure temp directory exists (local disk uses app/private/)
|
||||
$tempDir = storage_path('app/private/uploads/temp');
|
||||
if (! is_dir($tempDir)) {
|
||||
mkdir($tempDir, 0755, true);
|
||||
}
|
||||
|
||||
$storedName = Str::uuid().'.'.$extension;
|
||||
|
||||
try {
|
||||
$tempPath = $file->storeAs('uploads/temp', $storedName, ['disk' => 'local']);
|
||||
|
||||
if ($tempPath === false || $tempPath === null) {
|
||||
\Log::error('File storeAs returned false/null', [
|
||||
'original_name' => $originalName,
|
||||
'stored_name' => $storedName,
|
||||
'temp_dir' => $tempDir,
|
||||
'temp_dir_exists' => is_dir($tempDir),
|
||||
'temp_dir_writable' => is_writable($tempDir),
|
||||
]);
|
||||
return response()->json(['message' => 'Failed to store file'], 500);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('File storage exception', [
|
||||
'error' => $e->getMessage(),
|
||||
'original_name' => $originalName,
|
||||
'stored_name' => $storedName,
|
||||
]);
|
||||
return response()->json(['message' => 'Failed to store file: '.$e->getMessage()], 500);
|
||||
}
|
||||
|
||||
// Local disk stores in app/private/, so construct full path accordingly
|
||||
$fullPath = storage_path('app/private/'.$tempPath);
|
||||
|
||||
\Log::info('File stored successfully', [
|
||||
'temp_path' => $tempPath,
|
||||
'full_path' => $fullPath,
|
||||
'file_exists' => file_exists($fullPath),
|
||||
]);
|
||||
|
||||
$upload = $event->uploads()->create([
|
||||
'original_filename' => $originalName,
|
||||
'stored_filename' => $storedName,
|
||||
'file_size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'status' => 'pending',
|
||||
'uploader_name' => $request->input('uploader_name'),
|
||||
'uploader_email' => $request->input('uploader_email'),
|
||||
]);
|
||||
|
||||
ProcessEventUpload::dispatch($upload, $fullPath);
|
||||
|
||||
return response()->json([
|
||||
'upload_id' => $upload->id,
|
||||
'status' => $upload->status,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function uploadStatus(string $slug, int $uploadId): JsonResponse
|
||||
{
|
||||
$event = Event::where('slug', $slug)->where('is_active', true)->firstOrFail();
|
||||
|
||||
$upload = $event->uploads()->findOrFail($uploadId);
|
||||
|
||||
return response()->json([
|
||||
'id' => $upload->id,
|
||||
'status' => $upload->status,
|
||||
'error_message' => $upload->error_message,
|
||||
'google_drive_web_link' => $upload->google_drive_web_link,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function validateUploadWindow(Event $event): void
|
||||
{
|
||||
if ($event->upload_start_at && now()->isBefore($event->upload_start_at)) {
|
||||
abort(422, 'Uploads are not yet open.');
|
||||
}
|
||||
if ($event->upload_end_at && now()->isAfter($event->upload_end_at)) {
|
||||
abort(422, 'Upload window has closed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
26
api/app/Http/Middleware/ThrottlePasswordVerification.php
Normal file
26
api/app/Http/Middleware/ThrottlePasswordVerification.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ThrottlePasswordVerification
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$key = 'password-verify:'.$request->ip();
|
||||
|
||||
if (RateLimiter::tooManyAttempts($key, 5)) {
|
||||
$seconds = RateLimiter::availableIn($key);
|
||||
|
||||
return response()->json([
|
||||
'message' => "Too many attempts. Please try again in {$seconds} seconds.",
|
||||
], 429);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
32
api/app/Http/Requests/Admin/StoreEventRequest.php
Normal file
32
api/app/Http/Requests/Admin/StoreEventRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEventRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string', 'max:5000'],
|
||||
'slug' => ['nullable', 'string', 'max:100', 'unique:events,slug', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'google_drive_folder_id' => ['nullable', 'string', 'max:255'],
|
||||
'google_drive_folder_name' => ['nullable', 'string', 'max:255'],
|
||||
'is_active' => ['boolean'],
|
||||
'upload_start_at' => ['nullable', 'date'],
|
||||
'upload_end_at' => ['nullable', 'date', 'after:upload_start_at'],
|
||||
'max_file_size_mb' => ['integer', 'min:1', 'max:2000'],
|
||||
'allowed_extensions' => ['array'],
|
||||
'allowed_extensions.*' => ['string', 'in:mp4,mov,avi,mkv,webm,jpg,jpeg,png'],
|
||||
'require_password' => ['boolean'],
|
||||
'upload_password' => ['nullable', 'string', 'min:4', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
42
api/app/Http/Requests/Admin/UpdateEventRequest.php
Normal file
42
api/app/Http/Requests/Admin/UpdateEventRequest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateEventRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$event = $this->route('event');
|
||||
$eventId = $event?->id ?? $this->route('id');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string', 'max:5000'],
|
||||
'slug' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:100',
|
||||
'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/',
|
||||
Rule::unique('events', 'slug')->ignore($eventId),
|
||||
],
|
||||
'google_drive_folder_id' => ['nullable', 'string', 'max:255'],
|
||||
'google_drive_folder_name' => ['nullable', 'string', 'max:255'],
|
||||
'is_active' => ['boolean'],
|
||||
'upload_start_at' => ['nullable', 'date'],
|
||||
'upload_end_at' => ['nullable', 'date', 'after:upload_start_at'],
|
||||
'max_file_size_mb' => ['integer', 'min:1', 'max:2000'],
|
||||
'allowed_extensions' => ['array'],
|
||||
'allowed_extensions.*' => ['string', 'in:mp4,mov,avi,mkv,webm,jpg,jpeg,png'],
|
||||
'require_password' => ['boolean'],
|
||||
'upload_password' => ['nullable', 'string', 'min:4', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
20
api/app/Http/Requests/Public/VerifyPasswordRequest.php
Normal file
20
api/app/Http/Requests/Public/VerifyPasswordRequest.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Public;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class VerifyPasswordRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
69
api/app/Jobs/ProcessEventUpload.php
Normal file
69
api/app/Jobs/ProcessEventUpload.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Upload;
|
||||
use App\Services\GoogleDrive\GoogleDriveService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProcessEventUpload implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public function __construct(
|
||||
public Upload $upload,
|
||||
public string $tempFilePath
|
||||
) {}
|
||||
|
||||
public function handle(GoogleDriveService $googleDrive): void
|
||||
{
|
||||
$upload = $this->upload;
|
||||
$event = $upload->event;
|
||||
$user = $event->user;
|
||||
|
||||
$upload->update(['status' => 'uploading', 'upload_started_at' => now()]);
|
||||
|
||||
try {
|
||||
if (! $event->google_drive_folder_id) {
|
||||
throw new \RuntimeException('Event has no Google Drive folder configured.');
|
||||
}
|
||||
|
||||
if (! file_exists($this->tempFilePath)) {
|
||||
throw new \RuntimeException('Temporary file not found.');
|
||||
}
|
||||
|
||||
$result = $googleDrive->uploadFile(
|
||||
$user,
|
||||
$this->tempFilePath,
|
||||
$event->google_drive_folder_id,
|
||||
$upload->original_filename,
|
||||
$upload->mime_type
|
||||
);
|
||||
|
||||
$upload->update([
|
||||
'google_drive_file_id' => $result['id'],
|
||||
'google_drive_web_link' => $result['webViewLink'] ?? null,
|
||||
'status' => 'completed',
|
||||
'upload_completed_at' => now(),
|
||||
'error_message' => null,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
$upload->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
'upload_completed_at' => now(),
|
||||
]);
|
||||
throw $e;
|
||||
} finally {
|
||||
if (file_exists($this->tempFilePath)) {
|
||||
@unlink($this->tempFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
api/app/Models/Event.php
Normal file
69
api/app/Models/Event.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class Event extends Model
|
||||
{
|
||||
protected $hidden = ['upload_password'];
|
||||
|
||||
protected $appends = ['has_password'];
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'description',
|
||||
'slug',
|
||||
'google_drive_folder_id',
|
||||
'google_drive_folder_name',
|
||||
'is_active',
|
||||
'upload_start_at',
|
||||
'upload_end_at',
|
||||
'max_file_size_mb',
|
||||
'allowed_extensions',
|
||||
'upload_password',
|
||||
'require_password',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'upload_start_at' => 'datetime',
|
||||
'upload_end_at' => 'datetime',
|
||||
'is_active' => 'boolean',
|
||||
'require_password' => 'boolean',
|
||||
'allowed_extensions' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function getAllowedExtensionsAttribute($value): array
|
||||
{
|
||||
$decoded = $value ? json_decode($value, true) : null;
|
||||
|
||||
return is_array($decoded) ? $decoded : ['mp4', 'mov', 'avi', 'mkv', 'webm'];
|
||||
}
|
||||
|
||||
public function getHasPasswordAttribute(): bool
|
||||
{
|
||||
return ! empty($this->attributes['upload_password'] ?? null);
|
||||
}
|
||||
|
||||
public function setUploadPasswordAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['upload_password'] = $value ? Hash::make($value) : null;
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function uploads(): HasMany
|
||||
{
|
||||
return $this->hasMany(Upload::class);
|
||||
}
|
||||
}
|
||||
31
api/app/Models/GoogleDriveConnection.php
Normal file
31
api/app/Models/GoogleDriveConnection.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class GoogleDriveConnection extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'token_expires_at',
|
||||
'account_email',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'token_expires_at' => 'datetime',
|
||||
'access_token' => 'encrypted',
|
||||
'refresh_token' => 'encrypted',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
38
api/app/Models/Upload.php
Normal file
38
api/app/Models/Upload.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Upload extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'event_id',
|
||||
'original_filename',
|
||||
'stored_filename',
|
||||
'file_size',
|
||||
'mime_type',
|
||||
'google_drive_file_id',
|
||||
'google_drive_web_link',
|
||||
'status',
|
||||
'error_message',
|
||||
'uploader_name',
|
||||
'uploader_email',
|
||||
'upload_started_at',
|
||||
'upload_completed_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'upload_started_at' => 'datetime',
|
||||
'upload_completed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function event(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Event::class);
|
||||
}
|
||||
}
|
||||
59
api/app/Models/User.php
Normal file
59
api/app/Models/User.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function googleDriveConnections(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(GoogleDriveConnection::class);
|
||||
}
|
||||
|
||||
public function events(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Event::class);
|
||||
}
|
||||
}
|
||||
24
api/app/Providers/AppServiceProvider.php
Normal file
24
api/app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
294
api/app/Services/GoogleDrive/GoogleDriveService.php
Normal file
294
api/app/Services/GoogleDrive/GoogleDriveService.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\GoogleDrive;
|
||||
|
||||
use App\Models\GoogleDriveConnection;
|
||||
use App\Models\User;
|
||||
use Google\Client as GoogleClient;
|
||||
use Google\Http\MediaFileUpload;
|
||||
use Google\Service\Drive;
|
||||
use Google\Service\Drive\DriveFile;
|
||||
|
||||
class GoogleDriveService
|
||||
{
|
||||
protected const DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive';
|
||||
|
||||
protected const DRIVE_METADATA_SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly';
|
||||
|
||||
public function getAuthUrl(): string
|
||||
{
|
||||
$client = $this->createOAuthClient();
|
||||
$client->setState(optional(request()->user())->id);
|
||||
|
||||
return $client->createAuthUrl();
|
||||
}
|
||||
|
||||
public function handleCallback(string $code, User $user): GoogleDriveConnection
|
||||
{
|
||||
$client = $this->createOAuthClient();
|
||||
$token = $client->fetchAccessTokenWithAuthCode($code);
|
||||
|
||||
if (isset($token['error'])) {
|
||||
throw new \RuntimeException('Error fetching access token: '.($token['error_description'] ?? $token['error']));
|
||||
}
|
||||
|
||||
$expiresAt = now();
|
||||
if (isset($token['expires_in'])) {
|
||||
$expiresAt = now()->addSeconds($token['expires_in']);
|
||||
}
|
||||
|
||||
$connection = $user->googleDriveConnections()->first();
|
||||
$email = $this->getTokenEmail($token, $client);
|
||||
|
||||
if ($connection) {
|
||||
$connection->update([
|
||||
'access_token' => $token['access_token'] ?? $connection->access_token,
|
||||
'refresh_token' => $token['refresh_token'] ?? $connection->refresh_token,
|
||||
'token_expires_at' => $expiresAt,
|
||||
'account_email' => $email ?? $connection->account_email,
|
||||
]);
|
||||
} else {
|
||||
$connection = $user->googleDriveConnections()->create([
|
||||
'access_token' => $token['access_token'],
|
||||
'refresh_token' => $token['refresh_token'] ?? '',
|
||||
'token_expires_at' => $expiresAt,
|
||||
'account_email' => $email ?? 'unknown',
|
||||
]);
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
public function getClient(User $user): \Google\Client
|
||||
{
|
||||
$connection = $user->googleDriveConnections()->first();
|
||||
if (! $connection) {
|
||||
throw new \RuntimeException('No Google Drive connection found for user.');
|
||||
}
|
||||
|
||||
$client = $this->createOAuthClient();
|
||||
$token = [
|
||||
'access_token' => $connection->access_token,
|
||||
'refresh_token' => $connection->refresh_token,
|
||||
'created' => $connection->token_expires_at->subSeconds(3600)->timestamp,
|
||||
'expires_in' => 3600,
|
||||
];
|
||||
|
||||
$client->setAccessToken($token);
|
||||
|
||||
if ($connection->token_expires_at->isPast() || $client->isAccessTokenExpired()) {
|
||||
$newToken = $client->fetchAccessTokenWithRefreshToken($connection->refresh_token);
|
||||
if (isset($newToken['error'])) {
|
||||
throw new \RuntimeException('Error refreshing token: '.($newToken['error_description'] ?? $newToken['error']));
|
||||
}
|
||||
$expiresAt = isset($newToken['expires_in'])
|
||||
? now()->addSeconds($newToken['expires_in'])
|
||||
: now()->addHour();
|
||||
$connection->update([
|
||||
'access_token' => $newToken['access_token'],
|
||||
'token_expires_at' => $expiresAt,
|
||||
]);
|
||||
$client->setAccessToken($newToken);
|
||||
}
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
public function getDriveService(User $user): Drive
|
||||
{
|
||||
$client = $this->getClient($user);
|
||||
|
||||
return new Drive($client);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all Shared Drives (Team Drives) the user has access to.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<int, array{id: string, name: string, type: string}>
|
||||
*/
|
||||
public function listSharedDrives(User $user): \Illuminate\Support\Collection
|
||||
{
|
||||
$service = $this->getDriveService($user);
|
||||
$optParams = [
|
||||
'pageSize' => 100,
|
||||
'fields' => 'drives(id, name)',
|
||||
];
|
||||
$results = $service->drives->listDrives($optParams);
|
||||
|
||||
return collect($results->getDrives())->map(fn ($d) => [
|
||||
'id' => $d->getId(),
|
||||
'name' => $d->getName(),
|
||||
'type' => 'shared_drive',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, array{id: string, name: string}>
|
||||
*/
|
||||
public function listFolders(User $user, ?string $parentId = null, ?string $driveId = null): \Illuminate\Support\Collection
|
||||
{
|
||||
$service = $this->getDriveService($user);
|
||||
$query = "mimeType = 'application/vnd.google-apps.folder' and trashed = false";
|
||||
|
||||
if ($parentId) {
|
||||
// List folders inside a specific parent folder
|
||||
$query .= " and '{$parentId}' in parents";
|
||||
} elseif ($driveId) {
|
||||
// List root folders of the Shared Drive
|
||||
// For Shared Drives, we need to get folders where the drive itself is the parent
|
||||
// This is done by querying within the specific drive without a parent filter
|
||||
// and filtering to only top-level items (those with the drive as direct parent)
|
||||
// However, the API doesn't have a simple "root" concept for Shared Drives
|
||||
// So we'll fetch all and filter, or use a different approach
|
||||
|
||||
// Better approach: Get the drive root and list its children
|
||||
// We'll query for folders where parents contains the driveId
|
||||
$query .= " and '{$driveId}' in parents";
|
||||
} else {
|
||||
// List root folders of My Drive
|
||||
$query .= " and 'root' in parents";
|
||||
}
|
||||
|
||||
$optParams = [
|
||||
'q' => $query,
|
||||
'fields' => 'files(id, name)',
|
||||
'orderBy' => 'name',
|
||||
'supportsAllDrives' => true,
|
||||
'includeItemsFromAllDrives' => true,
|
||||
];
|
||||
|
||||
if ($driveId) {
|
||||
$optParams['driveId'] = $driveId;
|
||||
$optParams['corpora'] = 'drive';
|
||||
}
|
||||
|
||||
$results = $service->files->listFiles($optParams);
|
||||
|
||||
return collect($results->getFiles())->map(fn ($f) => ['id' => $f->getId(), 'name' => $f->getName()]);
|
||||
}
|
||||
|
||||
public function createFolder(User $user, string $name, ?string $parentId = null, ?string $driveId = null): array
|
||||
{
|
||||
$service = $this->getDriveService($user);
|
||||
$file = new DriveFile;
|
||||
$file->setName($name);
|
||||
$file->setMimeType('application/vnd.google-apps.folder');
|
||||
|
||||
if ($parentId) {
|
||||
$file->setParents([$parentId]);
|
||||
} elseif ($driveId) {
|
||||
// Creating in root of Shared Drive - no parent needed, just driveId
|
||||
$file->setDriveId($driveId);
|
||||
}
|
||||
|
||||
$optParams = [
|
||||
'supportsAllDrives' => true,
|
||||
];
|
||||
|
||||
$created = $service->files->create($file, $optParams);
|
||||
|
||||
return ['id' => $created->getId(), 'name' => $created->getName()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Google Drive. Returns array with 'id' and 'webViewLink'.
|
||||
*/
|
||||
public function uploadFile(User $user, string $filePath, string $folderId, string $filename, string $mimeType): array
|
||||
{
|
||||
$client = $this->getClient($user);
|
||||
$service = new Drive($client);
|
||||
|
||||
$file = new DriveFile;
|
||||
$file->setName($filename);
|
||||
$file->setParents([$folderId]);
|
||||
|
||||
$client->setDefer(true);
|
||||
$request = $service->files->create($file, [
|
||||
'mimeType' => $mimeType,
|
||||
'uploadType' => 'resumable',
|
||||
'fields' => 'id, webViewLink',
|
||||
'supportsAllDrives' => true,
|
||||
]);
|
||||
|
||||
$chunkSize = 5 * 1024 * 1024; // 5MB
|
||||
$media = new MediaFileUpload(
|
||||
$client,
|
||||
$request,
|
||||
$mimeType,
|
||||
'',
|
||||
true,
|
||||
$chunkSize
|
||||
);
|
||||
$media->setFileSize(filesize($filePath));
|
||||
$handle = fopen($filePath, 'rb');
|
||||
$status = false;
|
||||
while (! $status) {
|
||||
$chunk = fread($handle, $chunkSize);
|
||||
$status = $media->nextChunk($chunk);
|
||||
}
|
||||
fclose($handle);
|
||||
$client->setDefer(false);
|
||||
|
||||
$file = $status;
|
||||
if (! $file instanceof DriveFile) {
|
||||
throw new \RuntimeException('Upload did not return file metadata.');
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $file->getId(),
|
||||
'webViewLink' => $file->getWebViewLink() ?? $file->getWebContentLink() ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
public function deleteFile(User $user, string $fileId): void
|
||||
{
|
||||
$service = $this->getDriveService($user);
|
||||
$service->files->delete($fileId, ['supportsAllDrives' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a temporary download URL or web link for a file.
|
||||
*/
|
||||
public function getFileLink(User $user, string $fileId): ?string
|
||||
{
|
||||
$service = $this->getDriveService($user);
|
||||
$file = $service->files->get($fileId, [
|
||||
'fields' => 'webViewLink, webContentLink',
|
||||
'supportsAllDrives' => true,
|
||||
]);
|
||||
|
||||
return $file->getWebViewLink() ?? $file->getWebContentLink();
|
||||
}
|
||||
|
||||
protected function createOAuthClient(): GoogleClient
|
||||
{
|
||||
$client = new GoogleClient;
|
||||
$client->setClientId(config('services.google.client_id'));
|
||||
$client->setClientSecret(config('services.google.client_secret'));
|
||||
$client->setRedirectUri(config('services.google.redirect_uri'));
|
||||
$client->setScopes([self::DRIVE_SCOPE, self::DRIVE_METADATA_SCOPE]);
|
||||
$client->setAccessType('offline');
|
||||
$client->setPrompt('consent');
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
protected function getTokenEmail(array $token, GoogleClient $client): ?string
|
||||
{
|
||||
if (isset($token['id_token'])) {
|
||||
$payload = $client->verifyIdToken($token['id_token']);
|
||||
if ($payload && isset($payload['email'])) {
|
||||
return $payload['email'];
|
||||
}
|
||||
}
|
||||
try {
|
||||
$client->setAccessToken($token);
|
||||
$oauth2 = new \Google\Service\Oauth2($client);
|
||||
$info = $oauth2->userinfo->get();
|
||||
|
||||
return $info->getEmail();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user