feat: harden tenant settings and import pipeline
This commit is contained in:
@@ -3,12 +3,11 @@
|
|||||||
namespace App\Filament\Resources\EmotionResource\Pages;
|
namespace App\Filament\Resources\EmotionResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\EmotionResource;
|
use App\Filament\Resources\EmotionResource;
|
||||||
use App\Models\Emotion;
|
use App\Services\EmotionImportService;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\Page;
|
use Filament\Resources\Pages\Page;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class ImportEmotions extends Page
|
class ImportEmotions extends Page
|
||||||
@@ -41,7 +40,7 @@ class ImportEmotions extends Page
|
|||||||
}
|
}
|
||||||
|
|
||||||
$fullPath = Storage::disk('public')->path($path);
|
$fullPath = Storage::disk('public')->path($path);
|
||||||
[$ok, $fail] = $this->importEmotionsCsv($fullPath);
|
[$ok, $fail] = app(EmotionImportService::class)->import($fullPath);
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->success()
|
->success()
|
||||||
@@ -54,61 +53,4 @@ class ImportEmotions extends Page
|
|||||||
{
|
{
|
||||||
return __('admin.emotions.import.heading');
|
return __('admin.emotions.import.heading');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function importEmotionsCsv(string $file): array
|
|
||||||
{
|
|
||||||
$handle = fopen($file, 'r');
|
|
||||||
if (!$handle) {
|
|
||||||
return [0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = 0;
|
|
||||||
$fail = 0;
|
|
||||||
$headers = fgetcsv($handle, 0, ',');
|
|
||||||
if (!$headers) {
|
|
||||||
return [0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
$map = array_flip($headers);
|
|
||||||
|
|
||||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
|
||||||
try {
|
|
||||||
DB::transaction(function () use ($row, $map, &$ok) {
|
|
||||||
$nameDe = trim($row[$map['name_de']] ?? '');
|
|
||||||
$nameEn = trim($row[$map['name_en']] ?? '');
|
|
||||||
|
|
||||||
if (empty($nameDe) && empty($nameEn)) {
|
|
||||||
throw new \Exception('Name is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$emotion = Emotion::create([
|
|
||||||
'name' => ['de' => $nameDe, 'en' => $nameEn],
|
|
||||||
'icon' => $row[$map['icon']] ?? null,
|
|
||||||
'color' => $row[$map['color']] ?? null,
|
|
||||||
'description' => [
|
|
||||||
'de' => $row[$map['description_de']] ?? null,
|
|
||||||
'en' => $row[$map['description_en']] ?? null,
|
|
||||||
],
|
|
||||||
'sort_order' => (int)($row[$map['sort_order']] ?? 0),
|
|
||||||
'is_active' => (int)($row[$map['is_active']] ?? 1) ? 1 : 0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$eventTypes = $row[$map['event_types']] ?? '';
|
|
||||||
if ($eventTypes) {
|
|
||||||
$slugs = array_filter(array_map('trim', explode('|', $eventTypes)));
|
|
||||||
if ($slugs) {
|
|
||||||
$eventTypeIds = DB::table('event_types')->whereIn('slug', $slugs)->pluck('id')->all();
|
|
||||||
$emotion->eventTypes()->attach($eventTypeIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$ok++;
|
|
||||||
});
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$fail++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose($handle);
|
|
||||||
return [$ok, $fail];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,13 @@ namespace App\Http\Controllers\Api\Tenant;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Tenant\SettingsStoreRequest;
|
use App\Http\Requests\Tenant\SettingsStoreRequest;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SettingsController extends Controller
|
class SettingsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the tenant's settings.
|
* Get the tenant's settings.
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return JsonResponse
|
|
||||||
*/
|
*/
|
||||||
public function index(Request $request): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -32,20 +29,14 @@ class SettingsController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the tenant's settings.
|
* Update the tenant's settings.
|
||||||
*
|
|
||||||
* @param SettingsStoreRequest $request
|
|
||||||
* @return JsonResponse
|
|
||||||
*/
|
*/
|
||||||
public function update(SettingsStoreRequest $request): JsonResponse
|
public function update(SettingsStoreRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$tenant = $request->tenant;
|
$tenant = $request->tenant;
|
||||||
|
$settings = $request->validated()['settings'];
|
||||||
// Merge new settings with existing ones
|
|
||||||
$currentSettings = $tenant->settings ?? [];
|
|
||||||
$newSettings = array_merge($currentSettings, $request->validated()['settings']);
|
|
||||||
|
|
||||||
$tenant->update([
|
$tenant->update([
|
||||||
'settings' => $newSettings,
|
'settings' => $settings,
|
||||||
'settings_updated_at' => now(),
|
'settings_updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -53,7 +44,7 @@ class SettingsController extends Controller
|
|||||||
'message' => 'Settings erfolgreich aktualisiert.',
|
'message' => 'Settings erfolgreich aktualisiert.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'id' => $tenant->id,
|
'id' => $tenant->id,
|
||||||
'settings' => $newSettings,
|
'settings' => $settings,
|
||||||
'updated_at' => now()->toISOString(),
|
'updated_at' => now()->toISOString(),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@@ -61,9 +52,6 @@ class SettingsController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset tenant settings to defaults.
|
* Reset tenant settings to defaults.
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return JsonResponse
|
|
||||||
*/
|
*/
|
||||||
public function reset(Request $request): JsonResponse
|
public function reset(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -93,7 +81,7 @@ class SettingsController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Settings auf Standardwerte zurückgesetzt.',
|
'message' => 'Settings auf Standardwerte zurueckgesetzt.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'id' => $tenant->id,
|
'id' => $tenant->id,
|
||||||
'settings' => $defaultSettings,
|
'settings' => $defaultSettings,
|
||||||
@@ -104,9 +92,6 @@ class SettingsController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate custom domain availability.
|
* Validate custom domain availability.
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return JsonResponse
|
|
||||||
*/
|
*/
|
||||||
public function validateDomain(Request $request): JsonResponse
|
public function validateDomain(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -116,20 +101,25 @@ class SettingsController extends Controller
|
|||||||
return response()->json(['error' => 'Domain ist erforderlich.'], 400);
|
return response()->json(['error' => 'Domain ist erforderlich.'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple validation - in production, check DNS records or database uniqueness
|
if (! $this->isValidDomain($domain)) {
|
||||||
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/', $domain)) {
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'available' => false,
|
'available' => false,
|
||||||
'message' => 'Ungültiges Domain-Format.',
|
'message' => 'Ungueltiges Domain-Format.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if domain is already taken by another tenant
|
$taken = Tenant::where('custom_domain', $domain)
|
||||||
$taken = Tenant::where('custom_domain', $domain)->where('id', '!=', $request->tenant->id)->exists();
|
->where('id', '!=', $request->tenant->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'available' => ! $taken,
|
'available' => ! $taken,
|
||||||
'message' => $taken ? 'Domain ist bereits vergeben.' : 'Domain ist verfügbar.',
|
'message' => $taken ? 'Domain ist bereits vergeben.' : 'Domain ist verfuegbar.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isValidDomain(string $domain): bool
|
||||||
|
{
|
||||||
|
return filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -70,8 +70,12 @@ class TenantTokenGuard
|
|||||||
Auth::setUser($principal);
|
Auth::setUser($principal);
|
||||||
$request->setUserResolver(fn () => $principal);
|
$request->setUserResolver(fn () => $principal);
|
||||||
|
|
||||||
$request->merge(['tenant_id' => $tenant->id]);
|
$request->merge([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'tenant' => $tenant,
|
||||||
|
]);
|
||||||
$request->attributes->set('tenant_id', $tenant->id);
|
$request->attributes->set('tenant_id', $tenant->id);
|
||||||
|
$request->attributes->set('tenant', $tenant);
|
||||||
$request->attributes->set('decoded_token', $decoded);
|
$request->attributes->set('decoded_token', $decoded);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
|||||||
@@ -50,10 +50,15 @@ class ProfileUpdateRequest extends FormRequest
|
|||||||
],
|
],
|
||||||
|
|
||||||
'preferred_locale' => [
|
'preferred_locale' => [
|
||||||
'required',
|
'nullable',
|
||||||
'string',
|
'string',
|
||||||
Rule::in($supportedLocales),
|
Rule::in($supportedLocales),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class SettingsStoreRequest extends FormRequest
|
|||||||
'settings.features.event_checklist' => ['nullable', 'boolean'],
|
'settings.features.event_checklist' => ['nullable', 'boolean'],
|
||||||
'settings.features.custom_domain' => ['nullable', 'boolean'],
|
'settings.features.custom_domain' => ['nullable', 'boolean'],
|
||||||
'settings.features.advanced_analytics' => ['nullable', 'boolean'],
|
'settings.features.advanced_analytics' => ['nullable', 'boolean'],
|
||||||
'settings.custom_domain' => ['nullable', 'string', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/'],
|
'settings.custom_domain' => ['nullable', 'string', 'max:255', 'regex:/^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}$/'],
|
||||||
'settings.contact_email' => ['nullable', 'email', 'max:255'],
|
'settings.contact_email' => ['nullable', 'email', 'max:255'],
|
||||||
'settings.event_default_type' => ['nullable', 'string', 'max:50'],
|
'settings.event_default_type' => ['nullable', 'string', 'max:50'],
|
||||||
];
|
];
|
||||||
@@ -46,11 +46,11 @@ class SettingsStoreRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'settings.required' => 'Settings-Daten sind erforderlich.',
|
'settings.required' => 'Settings-Daten sind erforderlich.',
|
||||||
'settings.branding.logo_url.url' => 'Die Logo-URL muss eine gültige URL sein.',
|
'settings.branding.logo_url.url' => 'Die Logo-URL muss eine gOltige URL sein.',
|
||||||
'settings.branding.primary_color.regex' => 'Die Primärfarbe muss ein gültiges Hex-Format (#RRGGBB) haben.',
|
'settings.branding.primary_color.regex' => 'Die Prim??rfarbe muss ein gOltiges Hex-Format (#RRGGBB) haben.',
|
||||||
'settings.branding.secondary_color.regex' => 'Die Sekundärfarbe muss ein gültiges Hex-Format (#RRGGBB) haben.',
|
'settings.branding.secondary_color.regex' => 'Die Sekund??rfarbe muss ein gOltiges Hex-Format (#RRGGBB) haben.',
|
||||||
'settings.custom_domain.regex' => 'Das Custom Domain muss ein gültiges Domain-Format haben.',
|
'settings.custom_domain.regex' => 'Das Custom Domain muss ein gOltiges Domain-Format haben.',
|
||||||
'settings.contact_email.email' => 'Die Kontakt-E-Mail muss eine gültige E-Mail-Adresse sein.',
|
'settings.contact_email.email' => 'Die Kontakt-E-Mail muss eine gOltige E-Mail-Adresse sein.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,3 +64,4 @@ class SettingsStoreRequest extends FormRequest
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Resources\Tenant;
|
namespace App\Http\Resources\Tenant;
|
||||||
|
|
||||||
use App\Http\Resources\Tenant\EventResource;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -15,28 +14,27 @@ class TaskResource extends JsonResource
|
|||||||
*/
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
|
$assignedEventsCount = $this->relationLoaded('assignedEvents')
|
||||||
|
? $this->assignedEvents->count()
|
||||||
|
: $this->assignedEvents()->count();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
|
'tenant_id' => $this->tenant_id,
|
||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'priority' => $this->priority,
|
'priority' => $this->priority,
|
||||||
'due_date' => $this->due_date?->toISOString(),
|
'due_date' => $this->due_date?->toISOString(),
|
||||||
'is_completed' => $this->is_completed,
|
'is_completed' => (bool) $this->is_completed,
|
||||||
'collection_id' => $this->collection_id,
|
'collection_id' => $this->collection_id,
|
||||||
'assigned_events_count' => $this->assignedEvents()->count(),
|
'assigned_events_count' => $assignedEventsCount,
|
||||||
// TaskCollectionResource wird später implementiert
|
'assigned_events' => $this->whenLoaded(
|
||||||
// 'collection' => $this->whenLoaded('taskCollection', function () {
|
'assignedEvents',
|
||||||
// return new TaskCollectionResource($this->taskCollection);
|
fn () => EventResource::collection($this->assignedEvents)
|
||||||
// }),
|
),
|
||||||
'assigned_events' => $this->whenLoaded('assignedEvents', function () {
|
'created_at' => $this->created_at?->toISOString(),
|
||||||
return EventResource::collection($this->assignedEvents);
|
'updated_at' => $this->updated_at?->toISOString(),
|
||||||
}),
|
|
||||||
// UserResource wird später implementiert
|
|
||||||
// 'assigned_to' => $this->whenLoaded('assignedTo', function () {
|
|
||||||
// return new UserResource($this->assignedTo);
|
|
||||||
// }),
|
|
||||||
'created_at' => $this->created_at->toISOString(),
|
|
||||||
'updated_at' => $this->updated_at->toISOString(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Emotion extends Model
|
class Emotion extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'emotions';
|
protected $table = 'emotions';
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -25,3 +28,4 @@ class Emotion extends Model
|
|||||||
return $this->hasMany(Task::class);
|
return $this->hasMany(Task::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, BelongsToMany};
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Event extends Model
|
class Event extends Model
|
||||||
{
|
{
|
||||||
@@ -13,9 +15,7 @@ class Event extends Model
|
|||||||
protected $table = 'events';
|
protected $table = 'events';
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'date' => 'date',
|
'date' => 'datetime',
|
||||||
'name' => 'array',
|
|
||||||
'description' => 'array',
|
|
||||||
'settings' => 'array',
|
'settings' => 'array',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
];
|
];
|
||||||
@@ -39,5 +39,11 @@ class Event extends Model
|
|||||||
'task_collection_id'
|
'task_collection_id'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tasks(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Task::class, 'event_task', 'event_id', 'task_id')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class EventType extends Model
|
class EventType extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'event_types';
|
protected $table = 'event_types';
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -25,3 +28,4 @@ class EventType extends Model
|
|||||||
return $this->hasMany(Event::class);
|
return $this->hasMany(Event::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
|
|
||||||
class Photo extends Model
|
class Photo extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'photos';
|
protected $table = 'photos';
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -16,14 +18,12 @@ class Photo extends Model
|
|||||||
'metadata' => 'array',
|
'metadata' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Accessor für die Kompatibilität mit der PhotoResource
|
public function getImagePathAttribute(): ?string
|
||||||
public function getImagePathAttribute()
|
|
||||||
{
|
{
|
||||||
return $this->file_path;
|
return $this->file_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutator für die Kompatibilität mit der PhotoResource
|
public function setImagePathAttribute(string $value): void
|
||||||
public function setImagePathAttribute($value)
|
|
||||||
{
|
{
|
||||||
$this->attributes['file_path'] = $value;
|
$this->attributes['file_path'] = $value;
|
||||||
}
|
}
|
||||||
@@ -33,19 +33,19 @@ class Photo extends Model
|
|||||||
return $this->belongsTo(Event::class);
|
return $this->belongsTo(Event::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function emotion()
|
public function emotion(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Emotion::class);
|
return $this->belongsTo(Emotion::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function task()
|
public function task(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Task::class);
|
return $this->belongsTo(Task::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function likes(): HasMany
|
public function likes(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(\App\Models\PhotoLike::class);
|
return $this->hasMany(PhotoLike::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,21 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class PurchaseHistory extends Model
|
class PurchaseHistory extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'purchase_history';
|
protected $table = 'purchase_history';
|
||||||
|
public $timestamps = false;
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'id',
|
|
||||||
'tenant_id',
|
|
||||||
'package_id',
|
|
||||||
'credits_added',
|
|
||||||
'price',
|
|
||||||
'currency',
|
|
||||||
'platform',
|
|
||||||
'transaction_id',
|
|
||||||
'purchased_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'credits_added' => 'integer',
|
'credits_added' => 'integer',
|
||||||
'price' => 'decimal:2',
|
'price' => 'decimal:2',
|
||||||
@@ -35,3 +29,4 @@ class PurchaseHistory extends Model
|
|||||||
return $this->belongsTo(Tenant::class);
|
return $this->belongsTo(Tenant::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
class Task extends Model
|
class Task extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $table = 'tasks';
|
protected $table = 'tasks';
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'title' => 'array',
|
'due_date' => 'datetime',
|
||||||
'description' => 'array',
|
'is_completed' => 'bool',
|
||||||
'example_text' => 'array',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function emotion(): BelongsTo
|
public function emotion(): BelongsTo
|
||||||
|
|||||||
@@ -18,14 +18,6 @@ class TaskCollection extends Model
|
|||||||
'description',
|
'description',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'name' => 'array',
|
|
||||||
'description' => 'array',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tasks in this collection
|
|
||||||
*/
|
|
||||||
public function tasks(): BelongsToMany
|
public function tasks(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(
|
return $this->belongsToMany(
|
||||||
@@ -36,9 +28,6 @@ class TaskCollection extends Model
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Events that use this collection
|
|
||||||
*/
|
|
||||||
public function events(): BelongsToMany
|
public function events(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(
|
return $this->belongsToMany(
|
||||||
@@ -48,24 +37,5 @@ class TaskCollection extends Model
|
|||||||
'event_id'
|
'event_id'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the localized name for the current locale
|
|
||||||
*/
|
|
||||||
public function getLocalizedNameAttribute(): string
|
|
||||||
{
|
|
||||||
$locale = app()->getLocale();
|
|
||||||
return $this->name[$locale] ?? $this->name['de'] ?? 'Unnamed Collection';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the localized description for the current locale
|
|
||||||
*/
|
|
||||||
public function getLocalizedDescriptionAttribute(): ?string
|
|
||||||
{
|
|
||||||
if (!$this->description) return null;
|
|
||||||
|
|
||||||
$locale = app()->getLocale();
|
|
||||||
return $this->description[$locale] ?? $this->description['de'] ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use App\Models\EventPurchase;
|
|
||||||
use App\Models\EventCreditsLedger;
|
|
||||||
|
|
||||||
class Tenant extends Model
|
class Tenant extends Model
|
||||||
{
|
{
|
||||||
@@ -17,14 +15,16 @@ class Tenant extends Model
|
|||||||
|
|
||||||
protected $table = 'tenants';
|
protected $table = 'tenants';
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'settings' => 'array',
|
|
||||||
'features' => 'array',
|
'features' => 'array',
|
||||||
|
'settings' => 'array',
|
||||||
'last_activity_at' => 'datetime',
|
'last_activity_at' => 'datetime',
|
||||||
'event_credits_balance' => 'integer',
|
'event_credits_balance' => 'integer',
|
||||||
'subscription_tier' => 'string',
|
'subscription_tier' => 'string',
|
||||||
'subscription_expires_at' => 'datetime',
|
'subscription_expires_at' => 'datetime',
|
||||||
'total_revenue' => 'decimal:2',
|
'total_revenue' => 'decimal:2',
|
||||||
|
'settings_updated_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function events(): HasMany
|
public function events(): HasMany
|
||||||
@@ -37,10 +37,10 @@ class Tenant extends Model
|
|||||||
return $this->hasManyThrough(
|
return $this->hasManyThrough(
|
||||||
Photo::class,
|
Photo::class,
|
||||||
Event::class,
|
Event::class,
|
||||||
'tenant_id', // Foreign key on events table...
|
'tenant_id',
|
||||||
'event_id', // Foreign key on photos table...
|
'event_id',
|
||||||
'id', // Local key on tenants table...
|
'id',
|
||||||
'id' // Local key on events table...
|
'id'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +59,16 @@ class Tenant extends Model
|
|||||||
return $this->hasMany(EventCreditsLedger::class);
|
return $this->hasMany(EventCreditsLedger::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setSettingsAttribute($value): void
|
||||||
|
{
|
||||||
|
if (is_string($value)) {
|
||||||
|
$this->attributes['settings'] = $value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->attributes['settings'] = json_encode($value ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
public function activeSubscription(): Attribute
|
public function activeSubscription(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
|||||||
71
app/Services/EmotionImportService.php
Normal file
71
app/Services/EmotionImportService.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Emotion;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class EmotionImportService
|
||||||
|
{
|
||||||
|
public function import(string $filePath): array
|
||||||
|
{
|
||||||
|
$handle = fopen($filePath, 'r');
|
||||||
|
if (! $handle) {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = 0;
|
||||||
|
$failed = 0;
|
||||||
|
$headers = fgetcsv($handle, 0, ',');
|
||||||
|
if (! $headers) {
|
||||||
|
fclose($handle);
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = array_flip($headers);
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($row, $map, &$success) {
|
||||||
|
$nameDe = trim($row[$map['name_de']] ?? '');
|
||||||
|
$nameEn = trim($row[$map['name_en']] ?? '');
|
||||||
|
|
||||||
|
if ($nameDe === '' && $nameEn === '') {
|
||||||
|
throw new \RuntimeException('Name is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$emotion = Emotion::create([
|
||||||
|
'name' => ['de' => $nameDe, 'en' => $nameEn],
|
||||||
|
'icon' => $row[$map['icon']] ?? null,
|
||||||
|
'color' => $row[$map['color']] ?? null,
|
||||||
|
'description' => [
|
||||||
|
'de' => $row[$map['description_de']] ?? null,
|
||||||
|
'en' => $row[$map['description_en']] ?? null,
|
||||||
|
],
|
||||||
|
'sort_order' => (int) ($row[$map['sort_order']] ?? 0),
|
||||||
|
'is_active' => (int) ($row[$map['is_active']] ?? 1) ? 1 : 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$eventTypes = $row[$map['event_types']] ?? '';
|
||||||
|
if ($eventTypes) {
|
||||||
|
$slugs = array_filter(array_map('trim', explode('|', $eventTypes)));
|
||||||
|
if ($slugs) {
|
||||||
|
$ids = DB::table('event_types')->whereIn('slug', $slugs)->pluck('id')->all();
|
||||||
|
if ($ids) {
|
||||||
|
$emotion->eventTypes()->attach($ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$success++;
|
||||||
|
});
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return [$success, $failed];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
database/factories/EmotionFactory.php
Normal file
32
database/factories/EmotionFactory.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Emotion;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class EmotionFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Emotion::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$name = ucfirst($this->faker->unique()->word());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => [
|
||||||
|
'en' => $name,
|
||||||
|
'de' => $name,
|
||||||
|
],
|
||||||
|
'icon' => $this->faker->randomElement(['smile', 'heart', 'star', 'sparkles']),
|
||||||
|
'color' => $this->faker->hexColor(),
|
||||||
|
'description' => [
|
||||||
|
'en' => $this->faker->sentence(),
|
||||||
|
'de' => $this->faker->sentence(),
|
||||||
|
],
|
||||||
|
'sort_order' => $this->faker->numberBetween(0, 20),
|
||||||
|
'is_active' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
|
use App\Models\EventType;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -18,12 +19,14 @@ class EventFactory extends Factory
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'tenant_id' => Tenant::factory(),
|
'tenant_id' => Tenant::factory(),
|
||||||
|
'event_type_id' => EventType::factory(),
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'description' => $this->faker->paragraph(),
|
'description' => $this->faker->paragraph(),
|
||||||
'date' => $this->faker->dateTimeBetween('now', '+6 months'),
|
'date' => $this->faker->dateTimeBetween('now', '+6 months'),
|
||||||
'location' => $this->faker->address(),
|
'location' => $this->faker->address(),
|
||||||
'max_participants' => $this->faker->numberBetween(50, 500),
|
'max_participants' => $this->faker->numberBetween(50, 500),
|
||||||
|
'settings' => null,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'join_link_enabled' => true,
|
'join_link_enabled' => true,
|
||||||
'photo_upload_enabled' => true,
|
'photo_upload_enabled' => true,
|
||||||
@@ -61,3 +64,4 @@ class EventFactory extends Factory
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
database/factories/EventTypeFactory.php
Normal file
31
database/factories/EventTypeFactory.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\EventType;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class EventTypeFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = EventType::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$name = ucfirst($this->faker->unique()->words(2, true));
|
||||||
|
$slug = Str::slug($name);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => [
|
||||||
|
'de' => $name,
|
||||||
|
'en' => $name,
|
||||||
|
],
|
||||||
|
'slug' => $slug,
|
||||||
|
'icon' => $this->faker->randomElement(['party', 'camera', 'users', null]),
|
||||||
|
'settings' => [
|
||||||
|
'color' => $this->faker->hexColor(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
48
database/factories/PhotoFactory.php
Normal file
48
database/factories/PhotoFactory.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Emotion;
|
||||||
|
use App\Models\Event;
|
||||||
|
use App\Models\Photo;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class PhotoFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Photo::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'event_id' => Event::factory(),
|
||||||
|
'emotion_id' => Emotion::factory(),
|
||||||
|
'task_id' => null,
|
||||||
|
'guest_name' => $this->faker->name(),
|
||||||
|
'file_path' => 'photos/' . Str::uuid() . '.jpg',
|
||||||
|
'thumbnail_path' => 'photos/thumbnails/' . Str::uuid() . '.jpg',
|
||||||
|
'likes_count' => $this->faker->numberBetween(0, 25),
|
||||||
|
'is_featured' => false,
|
||||||
|
'metadata' => ['factory' => true],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure(): static
|
||||||
|
{
|
||||||
|
return $this->afterMaking(function (Photo $photo) {
|
||||||
|
if (! $photo->tenant_id && $photo->event) {
|
||||||
|
$photo->tenant_id = $photo->event->tenant_id;
|
||||||
|
}
|
||||||
|
})->afterCreating(function (Photo $photo) {
|
||||||
|
if ($photo->event && ! $photo->tenant_id) {
|
||||||
|
$photo->tenant_id = $photo->event->tenant_id;
|
||||||
|
$photo->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($photo->tenant_id && $photo->event && $photo->event->tenant_id !== $photo->tenant_id) {
|
||||||
|
$photo->event->update(['tenant_id' => $photo->tenant_id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
29
database/factories/PurchaseHistoryFactory.php
Normal file
29
database/factories/PurchaseHistoryFactory.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\PurchaseHistory;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class PurchaseHistoryFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = PurchaseHistory::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (string) Str::uuid(),
|
||||||
|
'tenant_id' => Tenant::factory(),
|
||||||
|
'package_id' => $this->faker->randomElement(['starter', 'pro', 'enterprise']),
|
||||||
|
'credits_added' => $this->faker->numberBetween(1, 10),
|
||||||
|
'price' => $this->faker->randomFloat(2, 9, 199),
|
||||||
|
'currency' => 'EUR',
|
||||||
|
'platform' => $this->faker->randomElement(['tenant-app', 'ios', 'android']),
|
||||||
|
'transaction_id' => (string) Str::uuid(),
|
||||||
|
'purchased_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,17 +14,19 @@ class TenantFactory extends Factory
|
|||||||
{
|
{
|
||||||
$name = $this->faker->company();
|
$name = $this->faker->company();
|
||||||
$slug = Str::slug($name);
|
$slug = Str::slug($name);
|
||||||
|
$contactEmail = $this->faker->companyEmail();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'contact_email' => $this->faker->companyEmail(),
|
'contact_email' => $contactEmail,
|
||||||
'event_credits_balance' => $this->faker->numberBetween(1, 20),
|
'event_credits_balance' => $this->faker->numberBetween(1, 20),
|
||||||
'subscription_tier' => $this->faker->randomElement(['free', 'starter', 'pro']),
|
'subscription_tier' => $this->faker->randomElement(['free', 'starter', 'pro']),
|
||||||
'subscription_expires_at' => $this->faker->dateTimeBetween('now', '+1 year'),
|
'subscription_expires_at' => $this->faker->dateTimeBetween('now', '+1 year'),
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'is_suspended' => false,
|
'is_suspended' => false,
|
||||||
'settings' => json_encode([
|
'settings_updated_at' => now(),
|
||||||
|
'settings' => [
|
||||||
'branding' => [
|
'branding' => [
|
||||||
'logo_url' => null,
|
'logo_url' => null,
|
||||||
'primary_color' => '#3B82F6',
|
'primary_color' => '#3B82F6',
|
||||||
@@ -38,8 +40,9 @@ class TenantFactory extends Factory
|
|||||||
'advanced_analytics' => false,
|
'advanced_analytics' => false,
|
||||||
],
|
],
|
||||||
'custom_domain' => null,
|
'custom_domain' => null,
|
||||||
]),
|
'contact_email' => $contactEmail,
|
||||||
'settings_updated_at' => now(),
|
'event_default_type' => 'general',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,3 +67,4 @@ class TenantFactory extends Factory
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tasks', function (Blueprint $table) {
|
||||||
|
if (! Schema::hasColumn('tasks', 'deleted_at')) {
|
||||||
|
$table->softDeletes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tasks', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('tasks', 'deleted_at')) {
|
||||||
|
$table->dropSoftDeletes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->string('custom_domain')->nullable()->after('domain');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->unique('custom_domain');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->dropUnique('tenants_custom_domain_unique');
|
||||||
|
$table->dropColumn('custom_domain');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,29 +2,26 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Filament\Resources\EmotionResource\Pages\ImportEmotions;
|
|
||||||
use App\Models\Emotion;
|
use App\Models\Emotion;
|
||||||
use App\Models\User;
|
use App\Services\EmotionImportService;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Livewire\Livewire;
|
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class EmotionResourceTest extends TestCase
|
class EmotionResourceTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_import_emotions_csv()
|
public function test_import_emotions_csv()
|
||||||
{
|
{
|
||||||
Storage::fake('public');
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
$csvData = "name_de,name_en,icon,color,description_de,description_en,sort_order,is_active,event_types\nGlück,Joy,😊,#FFD700,Gefühl des Glücks,Feeling of joy,1,1,wedding|birthday";
|
$csvData = "name_de,name_en,icon,color,description_de,description_en,sort_order,is_active,event_types\nGlück,Joy,😊,#FFD700,Gefühl des Glücks,Feeling of joy,1,1,wedding|birthday";
|
||||||
$csvFile = UploadedFile::fake()->createWithContent('emotions.csv', $csvData);
|
|
||||||
|
|
||||||
Livewire::test(ImportEmotions::class)
|
$tempFile = tempnam(sys_get_temp_dir(), 'emotions_');
|
||||||
->set('file', $csvFile->getRealPath())
|
file_put_contents($tempFile, $csvData);
|
||||||
->call('doImport');
|
|
||||||
|
[$success, $failed] = app(EmotionImportService::class)->import($tempFile);
|
||||||
|
|
||||||
|
$this->assertSame(1, $success);
|
||||||
|
$this->assertSame(0, $failed);
|
||||||
|
|
||||||
$this->assertDatabaseHas('emotions', [
|
$this->assertDatabaseHas('emotions', [
|
||||||
'name' => json_encode(['de' => 'Glück', 'en' => 'Joy']),
|
'name' => json_encode(['de' => 'Glück', 'en' => 'Joy']),
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class SettingsApiTest extends TenantTestCase
|
|||||||
|
|
||||||
$this->assertDatabaseHas('tenants', [
|
$this->assertDatabaseHas('tenants', [
|
||||||
'id' => $this->tenant->id,
|
'id' => $this->tenant->id,
|
||||||
'settings' => $settingsData['settings'],
|
'settings' => json_encode($settingsData['settings']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class SettingsApiTest extends TenantTestCase
|
|||||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/settings/reset');
|
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/settings/reset');
|
||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson(['message' => 'Settings auf Standardwerte zurückgesetzt.'])
|
->assertJson(['message' => 'Settings auf Standardwerte zurueckgesetzt.'])
|
||||||
->assertJsonPath('data.settings.branding.primary_color', '#3B82F6')
|
->assertJsonPath('data.settings.branding.primary_color', '#3B82F6')
|
||||||
->assertJsonPath('data.settings.features.photo_likes_enabled', true);
|
->assertJsonPath('data.settings.features.photo_likes_enabled', true);
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ class SettingsApiTest extends TenantTestCase
|
|||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson(['available' => true])
|
->assertJson(['available' => true])
|
||||||
->assertJson(['message' => 'Domain ist verfügbar.']);
|
->assertJson(['message' => 'Domain ist verfuegbar.']);
|
||||||
|
|
||||||
// Invalid domain format
|
// Invalid domain format
|
||||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/settings/validate-domain', [
|
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/settings/validate-domain', [
|
||||||
@@ -145,7 +145,7 @@ class SettingsApiTest extends TenantTestCase
|
|||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJson(['available' => false])
|
->assertJson(['available' => false])
|
||||||
->assertJson(['message' => 'Ungültiges Domain-Format.']);
|
->assertJson(['message' => 'Ungueltiges Domain-Format.']);
|
||||||
|
|
||||||
// Taken domain (create another tenant with same domain)
|
// Taken domain (create another tenant with same domain)
|
||||||
$otherTenant = Tenant::factory()->create(['custom_domain' => 'taken.example.com']);
|
$otherTenant = Tenant::factory()->create(['custom_domain' => 'taken.example.com']);
|
||||||
@@ -188,3 +188,5 @@ class SettingsApiTest extends TenantTestCase
|
|||||||
->assertJsonMissing(['#FF0000']); // Other tenant's color
|
->assertJsonMissing(['#FF0000']); // Other tenant's color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -263,7 +263,9 @@ class TaskApiTest extends TenantTestCase
|
|||||||
'tenant_id' => $this->tenant->id,
|
'tenant_id' => $this->tenant->id,
|
||||||
'priority' => 'medium',
|
'priority' => 'medium',
|
||||||
]);
|
]);
|
||||||
$collectionTasks->each(fn($task) => $task->taskCollection()->associate($collection));
|
$collectionTasks->each(function ($task) use ($collection) {
|
||||||
|
$task->update(['collection_id' => $collection->id]);
|
||||||
|
});
|
||||||
|
|
||||||
Task::factory(3)->create([
|
Task::factory(3)->create([
|
||||||
'tenant_id' => $this->tenant->id,
|
'tenant_id' => $this->tenant->id,
|
||||||
@@ -304,3 +306,7 @@ class TaskApiTest extends TenantTestCase
|
|||||||
->assertJsonPath('data.0.title', 'Search Test');
|
->assertJsonPath('data.0.title', 'Search Test');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Tenant;
|
namespace Tests\Feature\Tenant;
|
||||||
|
|
||||||
|
use App\Models\OAuthClient;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
abstract class TenantTestCase extends TestCase
|
abstract class TenantTestCase extends TestCase
|
||||||
@@ -15,42 +15,103 @@ abstract class TenantTestCase extends TestCase
|
|||||||
|
|
||||||
protected Tenant $tenant;
|
protected Tenant $tenant;
|
||||||
protected User $tenantUser;
|
protected User $tenantUser;
|
||||||
|
protected OAuthClient $oauthClient;
|
||||||
protected string $token;
|
protected string $token;
|
||||||
|
protected ?string $refreshToken = null;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->tenant = Tenant::factory()->create([
|
$this->initialiseTenantContext();
|
||||||
'name' => 'Test Tenant',
|
|
||||||
'slug' => 'test-tenant',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->tenantUser = User::factory()->create([
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => 'test@example.com',
|
|
||||||
'tenant_id' => $this->tenant->id,
|
|
||||||
'role' => 'admin',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->token = 'mock-jwt-token-' . $this->tenant->id . '-' . time();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function authenticatedRequest($method, $uri, array $data = [], array $headers = [])
|
protected function authenticatedRequest(string $method, string $uri, array $data = [], array $headers = [])
|
||||||
{
|
{
|
||||||
$headers['Authorization'] = 'Bearer ' . $this->token;
|
$headers = array_merge([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
// Temporarily override the middleware to skip auth and set tenant
|
], $headers);
|
||||||
$this->app['router']->pushMiddlewareToGroup('api', MockTenantMiddleware::class, 'mock-tenant');
|
|
||||||
|
|
||||||
return $this->withHeaders($headers)->json($method, $uri, $data);
|
return $this->withHeaders($headers)->json($method, $uri, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function mockTenantContext()
|
protected function mockTenantContext(): void
|
||||||
{
|
{
|
||||||
$this->actingAs($this->tenantUser);
|
|
||||||
|
|
||||||
// Set tenant globally for tests
|
|
||||||
$this->app->instance('tenant', $this->tenant);
|
$this->app->instance('tenant', $this->tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function initialiseTenantContext(array $scopes = ['tenant:read', 'tenant:write']): void
|
||||||
|
{
|
||||||
|
$this->tenant = Tenant::factory()->create([
|
||||||
|
'name' => 'Test Tenant',
|
||||||
|
'slug' => 'test-tenant-'.Str::random(6),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->tenantUser = User::factory()->create([
|
||||||
|
'name' => 'Test User',
|
||||||
|
'email' => 'test-'.Str::random(6).'@example.com',
|
||||||
|
'tenant_id' => $this->tenant->id,
|
||||||
|
'role' => 'admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->oauthClient = $this->createTenantClient($this->tenant, $scopes);
|
||||||
|
[$this->token, $this->refreshToken] = $this->issueTokens($this->oauthClient, $scopes);
|
||||||
|
|
||||||
|
$this->app->instance('tenant', $this->tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function createTenantClient(Tenant $tenant, array $scopes): OAuthClient
|
||||||
|
{
|
||||||
|
return OAuthClient::create([
|
||||||
|
'id' => (string) Str::uuid(),
|
||||||
|
'client_id' => 'tenant-admin-app-'.$tenant->id,
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'client_secret' => null,
|
||||||
|
'redirect_uris' => ['http://localhost/callback'],
|
||||||
|
'scopes' => $scopes,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function issueTokens(OAuthClient $client, array $scopes = ['tenant:read', 'tenant:write']): array
|
||||||
|
{
|
||||||
|
$codeVerifier = 'tenant-code-verifier-'.Str::random(32);
|
||||||
|
$codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
|
||||||
|
$state = Str::random(10);
|
||||||
|
|
||||||
|
$response = $this->get('/api/v1/oauth/authorize?'.http_build_query([
|
||||||
|
'client_id' => $client->client_id,
|
||||||
|
'redirect_uri' => 'http://localhost/callback',
|
||||||
|
'response_type' => 'code',
|
||||||
|
'scope' => implode(' ', $scopes),
|
||||||
|
'state' => $state,
|
||||||
|
'code_challenge' => $codeChallenge,
|
||||||
|
'code_challenge_method' => 'S256',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
$location = $response->headers->get('Location');
|
||||||
|
$this->assertNotNull($location);
|
||||||
|
|
||||||
|
$query = [];
|
||||||
|
parse_str(parse_url($location, PHP_URL_QUERY) ?? '', $query);
|
||||||
|
$authorizationCode = $query['code'] ?? null;
|
||||||
|
$this->assertNotNull($authorizationCode, 'Authorization code should be present');
|
||||||
|
|
||||||
|
$tokenResponse = $this->post('/api/v1/oauth/token', [
|
||||||
|
'grant_type' => 'authorization_code',
|
||||||
|
'code' => $authorizationCode,
|
||||||
|
'client_id' => $client->client_id,
|
||||||
|
'redirect_uri' => 'http://localhost/callback',
|
||||||
|
'code_verifier' => $codeVerifier,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tokenResponse->assertOk();
|
||||||
|
|
||||||
|
return [
|
||||||
|
$tokenResponse->json('access_token'),
|
||||||
|
$tokenResponse->json('refresh_token'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace Tests\Feature;
|
|||||||
use App\Models\OAuthClient;
|
use App\Models\OAuthClient;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -13,57 +12,6 @@ class TenantCreditsTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
private const PUBLIC_KEY = <<<KEY
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlrZWbp/7pXo83BIJX3v/
|
|
||||||
9f/51fxYFGZnZz9diqHkiOtDjggNdwze0LXruVeVb8YsaTI68RclgYCcsE4haTCG
|
|
||||||
LlTivKFJL2O10IEzswjjD08MsanHer3xZRO6VZ7JLXmBNKp5C71zfFf8AhMnQ+Y6
|
|
||||||
uGQ3wMOT6PWAiAmVBVYC8+KQsqyOkDu58bamhGGOrDsdWvrfDgRU1w8dxbgFYALQ
|
|
||||||
v1pVVmYT9oBxZcS5FlT8auf8zLcHXEl6S7X61ZPd/GTWT5htdSiJyXfSa/xM7bJP
|
|
||||||
CCv+mK6Gd5+1UG3RHGuwoi8Rch2O8PMglZqF6ybv/w836jUQKPl+sndePNN3soKQ
|
|
||||||
5wIDAQAB
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
KEY;
|
|
||||||
|
|
||||||
private const PRIVATE_KEY = <<<KEY
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWtlZun/ulejzc
|
|
||||||
Eglfe//1//nV/FgUZmdnP12KoeSI60OOCA13DN7Qteu5V5VvxixpMjrxFyWBgJyw
|
|
||||||
TiFpMIYuVOK8oUkvY7XQgTOzCOMPTwyxqcd6vfFlE7pVnskteYE0qnkLvXN8V/wC
|
|
||||||
EydD5jq4ZDfAw5Po9YCICZUFVgLz4pCyrI6QO7nxtqaEYY6sOx1a+t8OBFTXDx3F
|
|
||||||
uAVgAtC/WlVWZhP2gHFlxLkWVPxq5/zMtwdcSXpLtfrVk938ZNZPmG11KInJd9Jr
|
|
||||||
/Eztsk8IK/6YroZ3n7VQbdEca7CiLxFyHY7w8yCVmoXrJu//DzfqNRAo+X6yd148
|
|
||||||
03eygpDnAgMBAAECggEAFoldk11I/A2zXBU2YZjhRZ/pdB4v7Z0CiWXoTvq2eeL0
|
|
||||||
TyDVIqBCEWOixCxcpEI2EeT4+2RCr4LT62lDhb9D0VnQLfTQRM3cOjmXyYXirj9b
|
|
||||||
3pVMxwXwOvUgP/1mh+5La9yyDRdfVZCylnzWukiLL1eNHr4gOA2+EpmcNxgNiPp1
|
|
||||||
Z8USUp2kmSZMPmQDkGEAJnrqmW7LyBvda3yuW557WtpaQlHTprvNQdBIUoFhLiiS
|
|
||||||
HnV9kZfQHM3BdM06zx8c7W6sbVavLQlaD0mhM6Z7o7566pq1JKScjhfoGcZRTmLs
|
|
||||||
kshQVSf38ayhAz8CikWiJgqFJigIZI0bR9fROOy+wQKBgQDOWjVRq8Ql+Eu0so/B
|
|
||||||
3hS1TGaBOFe5vymeX+hnC87Zu7yVsj96mhmofnlTJdbSZLHfO631XD9O3qCcYzuK
|
|
||||||
1PLzOvO38ZVZLq/CkiwkC4qfGVQb3/8v0QyIXCKhMrwkwuL6AYMjQi6vd/+4vp2C
|
|
||||||
5EJefbNBfdvsC90t84wxqBpIDQKBgQC6+Rs7cBD9VOAKkNH1O4k9cE1JCDX6aqlg
|
|
||||||
RtO/93+kbqxz3llvIebI9z3CPE7Wp0n2GEFjvDCTy5kST7BQvdwm4VlthSpfhx+l
|
|
||||||
4ahw1+xbB3KQxemmf3MroTZWHLfTOGvHdei05EIdRZv8Mpi9UcHd7OhVO82SUnLn
|
|
||||||
pBqGLZGrwwKBgB2FiltE16sW+r2/ThHOU+gcJg4WoXZRgwLFddpINi+wTCqedbZ0
|
|
||||||
lXcloPXkU/eFsGzffOO9btE5yICXMc2K6bcil/uY9GTt6PdNMkN14z8fwIi8YyXU
|
|
||||||
Ipbfl5S4TXJ070QVM024CjXQVSV5H8+6GESsdxjHiM8cY2hPj58LDbeBAoGAfd5r
|
|
||||||
FcVoupJjzNkXbwboagLrFGpBpFYfth+YN1hPhou27r3V6TmiWtIOsm7VCC5QXSqR
|
|
||||||
AqpS7XwXjTs2T/Swe0AjatZF409c39gdA/JoPBO0bX++voZ4Kvv5T1k/6yLFc96N
|
|
||||||
jRFI7NnKm6oYJwMeBt+QvKhoyMNWdViFPqT4tu8CgYEAmcInq55jIJOr7GNvf6jV
|
|
||||||
wojrBxhEGOF8U8YqX6FgVEmVDkEOer3mFDnkZT/S2IFjH4eruo/ZTFFtyw9K9JGd
|
|
||||||
06FINYtK/H91SdcOJHuWdELuTQw0+Jtr47tSUlp1c3L0J7Mt1Sqqzg8lLoLYPcLJ
|
|
||||||
d7faJuYR8uKalWG3ZimbGNo=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
KEY;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
file_put_contents(storage_path('app/public.key'), self::PUBLIC_KEY);
|
|
||||||
file_put_contents(storage_path('app/private.key'), self::PRIVATE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_tenant_can_retrieve_balance_and_purchase_credits(): void
|
public function test_tenant_can_retrieve_balance_and_purchase_credits(): void
|
||||||
{
|
{
|
||||||
$tenant = Tenant::factory()->create([
|
$tenant = Tenant::factory()->create([
|
||||||
@@ -170,5 +118,5 @@ KEY;
|
|||||||
$tokenResponse->json('refresh_token'),
|
$tokenResponse->json('refresh_token'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,73 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
|||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
abstract class TestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
//
|
private const JWT_PUBLIC_KEY = <<<'KEY'
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlrZWbp/7pXo83BIJX3v/
|
||||||
|
9f/51fxYFGZnZz9diqHkiOtDjggNdwze0LXruVeVb8YsaTI68RclgYCcsE4haTCG
|
||||||
|
LlTivKFJL2O10IEzswjjD08MsanHer3xZRO6VZ7JLXmBNKp5C71zfFf8AhMnQ+Y6
|
||||||
|
uGQ3wMOT6PWAiAmVBVYC8+KQsqyOkDu58bamhGGOrDsdWvrfDgRU1w8dxbgFYALQ
|
||||||
|
v1pVVmYT9oBxZcS5FlT8auf8zLcHXEl6S7X61ZPd/GTWT5htdSiJyXfSa/xM7bJP
|
||||||
|
CCv+mK6Gd5+1UG3RHGuwoi8Rch2O8PMglZqF6ybv/w836jUQKPl+sndePNN3soKQ
|
||||||
|
5wIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
KEY;
|
||||||
|
|
||||||
|
private const JWT_PRIVATE_KEY = <<<'KEY'
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWtlZun/ulejzc
|
||||||
|
Eglfe//1//nV/FgUZmdnP12KoeSI60OOCA13DN7Qteu5V5VvxixpMjrxFyWBgJyw
|
||||||
|
TiFpMIYuVOK8oUkvY7XQgTOzCOMPTwyxqcd6vfFlE7pVnskteYE0qnkLvXN8V/wC
|
||||||
|
EydD5jq4ZDfAw5Po9YCICZUFVgLz4pCyrI6QO7nxtqaEYY6sOx1a+t8OBFTXDx3F
|
||||||
|
uAVgAtC/WlVWZhP2gHFlxLkWVPxq5/zMtwdcSXpLtfrVk938ZNZPmG11KInJd9Jr
|
||||||
|
/Eztsk8IK/6YroZ3n7VQbdEca7CiLxFyHY7w8yCVmoXrJu//DzfqNRAo+X6yd148
|
||||||
|
03eygpDnAgMBAAECggEAFoldk11I/A2zXBU2YZjhRZ/pdB4v7Z0CiWXoTvq2eeL0
|
||||||
|
TyDVIqBCEWOixCxcpEI2EeT4+2RCr4LT62lDhb9D0VnQLfTQRM3cOjmXyYXirj9b
|
||||||
|
3pVMxwXwOvUgP/1mh+5La9yyDRdfVZCylnzWukiLL1eNHr4gOA2+EpmcNxgNiPp1
|
||||||
|
Z8USUp2kmSZMPmQDkGEAJnrqmW7LyBvda3yuW557WtpaQlHTprvNQdBIUoFhLiiS
|
||||||
|
HnV9kZfQHM3BdM06zx8c7W6sbVavLQlaD0mhM6Z7o7566pq1JKScjhfoGcZRTmLs
|
||||||
|
kshQVSf38ayhAz8CikWiJgqFJigIZI0bR9fROOy+wQKBgQDOWjVRq8Ql+Eu0so/B
|
||||||
|
3hS1TGaBOFe5vymeX+hnC87Zu7yVsj96mhmofnlTJdbSZLHfO631XD9O3qCcYzuK
|
||||||
|
1PLzOvO38ZVZLq/CkiwkC4qfGVQb3/8v0QyIXCKhMrwkwuL6AYMjQi6vd/+4vp2C
|
||||||
|
5EJefbNBfdvsC90t84wxqBpIDQKBgQC6+Rs7cBD9VOAKkNH1O4k9cE1JCDX6aqlg
|
||||||
|
RtO/93+kbqxz3llvIebI9z3CPE7Wp0n2GEFjvDCTy5kST7BQvdwm4VlthSpfhx+l
|
||||||
|
4ahw1+xbB3KQxemmf3MroTZWHLfTOGvHdei05EIdRZv8Mpi9UcHd7OhVO82SUnLn
|
||||||
|
pBqGLZGrwwKBgB2FiltE16sW+r2/ThHOU+gcJg4WoXZRgwLFddpINi+wTCqedbZ0
|
||||||
|
lXcloPXkU/eFsGzffOO9btE5yICXMc2K6bcil/uY9GTt6PdNMkN14z8fwIi8YyXU
|
||||||
|
Ipbfl5S4TXJ070QVM024CjXQVSV5H8+6GESsdxjHiM8cY2hPj58LDbeBAoGAfd5r
|
||||||
|
FcVoupJjzNkXbwboagLrFGpBpFYfth+YN1hPhou27r3V6TmiWtIOsm7VCC5QXSqR
|
||||||
|
AqpS7XwXjTs2T/Swe0AjatZF409c39gdA/JoPBO0bX++voZ4Kvv5T1k/6yLFc96N
|
||||||
|
jRFI7NnKm6oYJwMeBt+QvKhoyMNWdViFPqT4tu8CgYEAmcInq55jIJOr7GNvf6jV
|
||||||
|
wojrBxhEGOF8U8YqX6FgVEmVDkEOer3mFDnkZT/S2IFjH4eruo/ZTFFtyw9K9JGd
|
||||||
|
06FINYtK/H91SdcOJHuWdELuTQw0+Jtr47tSUlp1c3L0J7Mt1Sqqzg8lLoLYPcLJ
|
||||||
|
d7faJuYR8uKalWG3ZimbGNo=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
KEY;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->ensureJwtKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function ensureJwtKeys(): void
|
||||||
|
{
|
||||||
|
$storagePath = storage_path('app');
|
||||||
|
|
||||||
|
if (! is_dir($storagePath)) {
|
||||||
|
mkdir($storagePath, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$publicKeyPath = $storagePath . DIRECTORY_SEPARATOR . 'public.key';
|
||||||
|
if (! file_exists($publicKeyPath)) {
|
||||||
|
file_put_contents($publicKeyPath, self::JWT_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
$privateKeyPath = $storagePath . DIRECTORY_SEPARATOR . 'private.key';
|
||||||
|
if (! file_exists($privateKeyPath)) {
|
||||||
|
file_put_contents($privateKeyPath, self::JWT_PRIVATE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user