π΅ MUZΔ°BU MΓZΔ°K PLATFORMU
SUBSCRIPTION & DEVICE LIMIT SΔ°STEMΔ° - MASTER DΓZENLEME PLANI
1. ΓYELΔ°K HΔ°YERARΕΔ°SΔ° VE DΔ°NLEME HAKLARI
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β KULLANICI TΓRLERΔ° β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β GUEST (Γye deΔil) β 30 saniye preview β
β ΓCRETSΔ°Z ΓYE β 30 saniye preview β
β PREMIUM / TRIAL ΓYE β SΔ±nΔ±rsΔ±z dinleme β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MEVCUT HATA:
Guest ve ΓΌcretsiz ΓΌye ayrΔ±mΔ± kodda tam yapΔ±lmamΔ±Ε!
Dosya: SongStreamController.php
SatΔ±r: 52-149
Sorun: Guest ($user == null) ve non-premium ($user->isPremiumOrTrial() == false) iΓ§in AYNI kod bloΔu tekrarlanΔ±yor.
β
ΓΓZΓM:
Helper method oluΕtur: getPreviewStreamResponse($song, $userType)
protected function getPreviewStreamResponse(Song $song, string $userType): JsonResponse
{
// HLS conversion baΕlat
if ($song->needsHlsConversion()) {
ConvertToHLSJob::dispatch($song);
}
// Stream URL hazΔ±rla
if (!empty($song->hls_path)) {
$streamUrl = route('api.muzibu.songs.dynamic-playlist', ['id' => $song->song_id]);
$streamType = 'hls';
$fallbackUrl = $this->signedUrlService->generateStreamUrl($song->song_id, 30, true);
} else {
$streamUrl = $this->signedUrlService->generateStreamUrl($song->song_id, 30);
$streamType = 'mp3';
$fallbackUrl = null;
}
$message = $userType === 'guest'
? 'KayΔ±t olun, tam dinleyin'
: 'Premium\'a geΓ§in, sΔ±nΔ±rsΔ±z dinleyin';
return response()->json([
'status' => 'preview',
'message' => $message,
'stream_url' => $streamUrl,
'stream_type' => $streamType,
'fallback_url' => $fallbackUrl,
'preview_duration' => 30,
'is_premium' => false,
]);
}
2. CΔ°HAZ LΔ°MΔ°TΔ° FALLBACK SΔ°STEMΔ° (3 Seviyeli)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CΔ°HAZ LΔ°MΔ°TΔ° BELΔ°RLEME SIRASI β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. User->device_limit β VIP/Test/Ban ΓΆzel durumlar β
β 2. SubscriptionPlan->device_limit β Premium plan limiti β
β 3. Setting('auth_device_limit') β Tenant global ayar β
β 4. Fallback: 1 cihaz β HiΓ§biri yoksa β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
DOΔRU UYGULAMA (MEVCUT):
Dosya: DeviceService.php:220-252
Method: getDeviceLimit(User $user)
public function getDeviceLimit(User $user): int
{
// 1. User override (VIP/Test/Ban)
if ($user->device_limit !== null && $user->device_limit > 0) {
return $user->device_limit;
}
// 2. Subscription Plan device_limit
$subscription = $user->subscriptions()
->whereIn('status', ['active', 'trial'])
->where(function($q) {
$q->whereNull('current_period_end')
->orWhere('current_period_end', '>', now());
})
->with('plan')
->first();
if ($subscription && $subscription->plan && $subscription->plan->device_limit) {
return (int) $subscription->plan->device_limit;
}
// 3. Tenant setting fallback
if (function_exists('setting') && setting('auth_device_limit')) {
return (int) setting('auth_device_limit');
}
// 4. Ultimate fallback
return 1;
}
β οΈ DΔ°KKAT:
BROWSER + SΔ°STEM ANIMI: Mevcut sistemde device detection Jenssegers\Agent ile yapΔ±lΔ±yor.
KayΔ±t Edilen Bilgiler:
- device_type: desktop / mobile / tablet
- device_name: "Windows 11 - Chrome"
- browser: Chrome, Firefox, Safari
- platform: Windows, macOS, Android, iOS
- ip_address: KullanΔ±cΔ± IP
3. LOGIN AKIΕI (Cihaz Limit KontrolΓΌ)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. KullanΔ±cΔ± email/password girer β
β 2. Credentials doΔrulanΔ±r β
β 3. DeviceService.checkDeviceLimitBeforeLogin() Γ§aΔrΔ±lΔ±r β
β βββ Limit AΕILMADI β Normal login devam β
β βββ Limit AΕILDI β 403 + device_limit_exceeded response β
β 4. Frontend Device Selection Modal aΓ§ar β
β βββ Aktif cihazlar listelenir (IP, Browser, Platform, Son) β
β βββ KullanΔ±cΔ± Γ§Δ±karmak istediΔi cihazΔ± seΓ§er β
β 5. terminateDevice() API Γ§aΔrΔ±lΔ±r (credential-based) β
β 6. SeΓ§ilen cihazΔ±n session'Δ± silinir β
β 7. KullanΔ±cΔ± tekrar login yapar (artΔ±k limit altΔ±nda) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MEVCUT HATA 1: CSRF Token Mismatch
Sorun: SPA (Single Page Application) yapΔ±sΔ±nda CSRF token yenilenmesi eksik!
Senaryo:
1. KullanΔ±cΔ± sayfayΔ± aΓ§ar (CSRF token alΔ±r)
2. Uzun sΓΌre bekler (session timeout)
3. Login yapmaya Γ§alΔ±ΕΔ±r
4. β CSRF token mismatch hatasΔ±!
β
ΓΓZΓM: Otomatik CSRF Token Yenileme
Dosya: resources/views/themes/muzibu/layouts/app.blade.php
// CSRF token yenileme interceptor ekle
axios.interceptors.response.use(
response => response,
error => {
// CSRF token mismatch hatasΔ±
if (error.response?.status === 419) {
console.warn('CSRF token expired, refreshing...');
// Yeni token al
return axios.get('/sanctum/csrf-cookie').then(() => {
// Token yenilendi, isteΔi tekrar gΓΆnder
return axios(error.config);
});
}
return Promise.reject(error);
}
);
β MEVCUT HATA 2: Device Selection Modal Eksik
Sorun: checkDeviceLimitBeforeLogin() backend'de var ama frontend modal entegrasyonu yok!
Dosya: DeviceService.php:282-321
Return: can_login, devices[], limit dΓΆndΓΌrΓΌyor ama frontend kullanmΔ±yor!
β
ΓΓZΓM: Frontend Login Flow GΓΌncelle
Dosya: public/themes/muzibu/js/features/auth.js
async login() {
try {
const response = await axios.post('/api/login', this.loginForm);
// Login baΕarΔ±lΔ±
window.location.reload();
} catch (error) {
// CSRF token yenileme zaten interceptor'da
// Device limit kontrolΓΌ
if (error.response?.status === 403 &&
error.response?.data?.error === 'device_limit_exceeded') {
// Device selection modal aΓ§
this.activeDevices = error.response.data.devices;
this.deviceLimit = error.response.data.limit;
this.showDeviceSelectionModal = true;
return;
}
// DiΔer hatalar
this.authError = error.response?.data?.message || 'GiriΕ baΕarΔ±sΔ±z';
}
}
4. SESSION POLLING SΔ°STEMΔ° (30 Saniye)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SESSION POLLING AKIΕI (Her 30 Saniye) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. KullanΔ±cΔ± giriΕ yapar β Redis session baΕlar β
β 2. Frontend setInterval(30000) baΕlatΔ±r β
β 3. Her 30 saniyede /api/device/ping Γ§aΔrΔ±lΔ±r β
β 4. DeviceService::updateSessionActivity() Γ§alΔ±ΕΔ±r β
β βββ user_active_sessions.last_activity gΓΌncellenir β
β βββ Redis session TTL yenilenir (120 dakika) β
β 5. Response: { device_limit_status, active_devices } β
β 6. EΔer device_limit_exceeded β Modal gΓΆster β
β 7. KullanΔ±cΔ± logout olursa β Polling durdur β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
DOΔRU UYGULAMA (MEVCUT):
Dosya: DeviceService.php:110-151
Method: updateSessionActivity()
public function updateSessionActivity(): array
{
$user = auth('web')->user() ?? auth('sanctum')->user();
if (!$user) {
return ['status' => 'unauthenticated'];
}
$sessionId = session()->getId();
// MySQL: last_activity gΓΌncelle
UserActiveSession::where('session_id', $sessionId)
->where('user_id', $user->id)
->update(['last_activity' => now()]);
// Redis: Session TTL yenile (120 dakika)
Redis::expire("laravel_session:{$sessionId}", 7200);
// Mevcut cihaz limiti kontrol
$deviceLimit = $this->getDeviceLimit($user);
$activeDevices = $this->getActiveDevicesCount($user);
return [
'status' => 'active',
'device_limit' => $deviceLimit,
'active_devices' => $activeDevices,
'device_limit_exceeded' => $activeDevices > $deviceLimit,
];
}
β οΈ DΔ°KKAT: HARDCODED DEΔER!
Sorun: Polling interval frontend'de hardcoded: 30000 ms
Dosya: public/themes/muzibu/js/player/core/player-core.js
SatΔ±r: ~850 (setInterval iΓ§inde)
β
ΓΓZΓM: Config DosyasΔ±na TaΕΔ±
Yeni Dosya: config/muzibu.php
return [
'session' => [
'polling_interval' => env('MUZIBU_SESSION_POLLING', 30000), // ms
'ttl' => env('MUZIBU_SESSION_TTL', 7200), // saniye
],
];
Blade Template:
window.muzibuPlayerConfig = {
sessionPollingInterval: {{ config('muzibu.session.polling_interval') }},
};
5. STREAM AKIΕI (HLS + MP3 Fallback)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β STREAM URL Δ°STEΔΔ° AKIΕI β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. Frontend /api/muzibu/songs/{id}/stream Γ§aΔrΔ±sΔ± yapar β
β 2. SongStreamController::stream() method Γ§alΔ±ΕΔ±r β
β 3. User kontrolΓΌ: β
β βββ Guest ($user == null) β
β β βββ getPreviewStreamResponse($song, 'guest') β
β βββ Free Member (!isPremiumOrTrial()) β
β β βββ getPreviewStreamResponse($song, 'free') β
β βββ Premium/Trial Member (isPremiumOrTrial()) β
β βββ Full stream access β
β 4. HLS kontrolΓΌ: β
β βββ hls_path VARSA β HLS playlist dΓΆndΓΌr β
β β βββ Timeout: 6 saniye β
β β βββ Fallback: MP3 signed URL β
β βββ hls_path YOKSA β ConvertToHLSJob dispatch et β
β 5. Response: stream_url, stream_type, fallback_url β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MEVCUT HATA: DUPLICATE CODE
Guest ve Free Member iΓ§in aynΔ± kod bloΔu tekrarlanΔ±yor!
Dosya: SongStreamController.php
SatΔ±r 52-96: Guest user stream logic (48 satΔ±r)
SatΔ±r 100-149: Non-premium user stream logic (49 satΔ±r)
Sorun: AYNI kod bloΔu kopyalanmΔ±Ε! (DRY ihlali)
β
ΓΓZΓM: Helper Method OluΕtur
Yeni Method: getPreviewStreamResponse()
public function stream(Song $song)
{
$user = auth('web')->user() ?? auth('sanctum')->user();
// 1. Guest user
if (!$user) {
return $this->getPreviewStreamResponse($song, 'guest');
}
// 2. Free member
if (!$user->isPremiumOrTrial()) {
return $this->getPreviewStreamResponse($song, 'free');
}
// 3. Premium/Trial member
return $this->getFullStreamResponse($song, $user);
}
protected function getPreviewStreamResponse(Song $song, string $userType): JsonResponse
{
// HLS conversion
if ($song->needsHlsConversion()) {
ConvertToHLSJob::dispatch($song);
}
// Stream URL (30 saniye limit)
if (!empty($song->hls_path)) {
$streamUrl = route('api.muzibu.songs.dynamic-playlist', [
'id' => $song->song_id
]);
$streamType = 'hls';
$fallbackUrl = $this->signedUrlService->generateStreamUrl(
$song->song_id, 30, true
);
} else {
$streamUrl = $this->signedUrlService->generateStreamUrl(
$song->song_id, 30
);
$streamType = 'mp3';
$fallbackUrl = null;
}
$message = $userType === 'guest'
? 'KayΔ±t olun, tam dinleyin'
: 'Premium\'a geΓ§in, sΔ±nΔ±rsΔ±z dinleyin';
return response()->json([
'status' => 'preview',
'message' => $message,
'stream_url' => $streamUrl,
'stream_type' => $streamType,
'fallback_url' => $fallbackUrl,
'preview_duration' => 30,
'is_premium' => false,
]);
}
protected function getFullStreamResponse(Song $song, User $user): JsonResponse
{
// Full stream logic (sΔ±nΔ±rsΔ±z)
// ...
}
β οΈ DΔ°KKAT: HARDCODED DEΔER!
Preview duration: 30 saniye hardcoded (4 yerde!)
HLS timeout: 6 saniye hardcoded
Chunk count: 3 chunk hardcoded
β
ΓΓZΓM: Config DosyasΔ±na TaΕΔ±
Dosya: config/muzibu.php
return [
'stream' => [
'preview_duration' => env('MUZIBU_PREVIEW_DURATION', 30), // saniye
'hls_timeout' => env('MUZIBU_HLS_TIMEOUT', 6), // saniye
'preview_chunks' => env('MUZIBU_PREVIEW_CHUNKS', 3), // adet
],
];
KullanΔ±m:
$previewDuration = config('muzibu.stream.preview_duration');
$this->signedUrlService->generateStreamUrl($song->song_id, $previewDuration);
6. PREMIUM STATUS DEΔΔ°ΕΔ°KLΔ°ΔΔ° (Cache Invalidation)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PREMIUM STATUS CACHE AKIΕI β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. User::isPremiumOrTrial() Γ§aΔrΔ±sΔ± yapΔ±lΔ±r β
β 2. Cache key: premium_status_{user_id}_{tenant_id} β
β 3. Cache TTL: 5 dakika (300 saniye) β
β 4. Cache HIT β Direkt return β
β 5. Cache MISS β DB sorgusu + Cache write β
β β
β SORUN: Subscription deΔiΕtiΔinde cache SΔ°LΔ°NMΔ°YOR! β
β βββ Yeni ΓΌyelik baΕlatΔ±lΔ±r β 5 dakika premium gΓΆrmez! β
β βββ Γyelik iptal edilir β 5 dakika premium gΓΆrmeye devam! β
β βββ Γyelik sΓΌresi biter β 5 dakika premium gΓΆrmeye devam! β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MEVCUT HATA: SUBSCRIPTION OBSERVER EKSΔ°K!
Sorun: Subscription deΔiΕikliklerinde cache otomatik silinmiyor!
Eksik Dosya: app/Observers/SubscriptionObserver.php
SonuΓ§: Premium status 5 dakika gecikmeyle gΓΌncelleniyor!
β
ΓΓZΓM: SubscriptionObserver OluΕtur
Yeni Dosya: app/Observers/SubscriptionObserver.php
namespace App\Observers;
use Modules\Subscription\app\Models\Subscription;
use Illuminate\Support\Facades\Cache;
class SubscriptionObserver
{
public function created(Subscription $subscription)
{
$this->flushPremiumCache($subscription->user_id);
}
public function updated(Subscription $subscription)
{
$this->flushPremiumCache($subscription->user_id);
}
public function deleted(Subscription $subscription)
{
$this->flushPremiumCache($subscription->user_id);
}
protected function flushPremiumCache(int $userId)
{
$tenantId = tenant() ? tenant()->id : 1;
$cacheKey = "premium_status_{$userId}_{$tenantId}";
Cache::forget($cacheKey);
\Log::info("Premium cache flushed", [
'user_id' => $userId,
'tenant_id' => $tenantId,
'cache_key' => $cacheKey,
]);
}
}
Register Observer:
Dosya: app/Providers/AppServiceProvider.php
use Modules\Subscription\app\Models\Subscription;
use App\Observers\SubscriptionObserver;
public function boot()
{
Subscription::observe(SubscriptionObserver::class);
}
β
EK ΓΓZΓM: Logout'ta da Cache Temizle
Dosya: app/Http/Controllers/Api/Auth/AuthController.php
public function logout()
{
$user = auth('web')->user() ?? auth('sanctum')->user();
if ($user) {
// Premium cache temizle
$tenantId = tenant() ? tenant()->id : 1;
$cacheKey = "premium_status_{$user->id}_{$tenantId}";
Cache::forget($cacheKey);
// Device session temizle
app(DeviceService::class)->unregisterSession();
// Auth logout
Auth::logout();
}
return response()->json(['message' => 'Logged out']);
}
7. HARDCODED DEΔERLER (Config'e TaΕΔ±nacaklar)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HARDCODED DEΔERLER LΔ°STESΔ° β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. Preview Duration: 30 saniye (4 yerde hardcoded!) β
β 2. HLS Timeout: 6 saniye β
β 3. Preview Chunks: 3 adet β
β 4. Session Polling: 30000 ms (frontend) β
β 5. Session TTL: 7200 saniye (Redis) β
β 6. Premium Cache TTL: 300 saniye (5 dakika) β
β 7. Device Limit Fallback: 1 cihaz β
β 8. Frontend deviceLimit: 1 (hardcoded!) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MEVCUT HATA 1: Frontend Device Limit Hardcoded
Dosya: public/themes/muzibu/js/player/core/player-core.js
SatΔ±r: ~40
Kod: deviceLimit: 1
β
ΓΓZΓM: Backend'den Δ°njection
Dosya: resources/views/themes/muzibu/layouts/app.blade.php
@auth
@php
$deviceService = app(\Modules\Muzibu\app\Services\DeviceService::class);
$deviceLimit = $deviceService->getDeviceLimit(auth()->user());
@endphp
window.muzibuPlayerConfig = {
deviceLimit: {{ $deviceLimit }},
sessionPollingInterval: {{ config('muzibu.session.polling_interval') }},
previewDuration: {{ config('muzibu.stream.preview_duration') }},
};
@endauth
Frontend KullanΔ±mΔ±:
// player-core.js
const playerConfig = {
deviceLimit: window.muzibuPlayerConfig?.deviceLimit || 1,
sessionPollingInterval: window.muzibuPlayerConfig?.sessionPollingInterval || 30000,
previewDuration: window.muzibuPlayerConfig?.previewDuration || 30,
};
β
ΓΓZΓM: Merkezi Config DosyasΔ± OluΕtur
Yeni Dosya: config/muzibu.php
return [
// Stream Settings
'stream' => [
'preview_duration' => env('MUZIBU_PREVIEW_DURATION', 30),
'hls_timeout' => env('MUZIBU_HLS_TIMEOUT', 6),
'preview_chunks' => env('MUZIBU_PREVIEW_CHUNKS', 3),
],
// Session Settings
'session' => [
'polling_interval' => env('MUZIBU_SESSION_POLLING', 30000),
'ttl' => env('MUZIBU_SESSION_TTL', 7200),
],
// Cache Settings
'cache' => [
'premium_status_ttl' => env('MUZIBU_PREMIUM_CACHE_TTL', 300),
],
// Device Settings
'device' => [
'default_limit' => env('MUZIBU_DEVICE_LIMIT_FALLBACK', 1),
],
];
8. SETTINGS ENTEGRASYONU (Tenant AyarlarΔ±)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TENANT SETTINGS SΔ°STEMΔ° β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β MEVCUT SETTINGS (KullanΔ±lΔ±yor): β
β ββ ID: 23 β auth_device_limit (Cihaz limit) β
β ββ ID: 21 β auth_session_lifetime (Session sΓΌre) β
β β
β EKLENECEK SETTINGS: β
β ββ muzibu_preview_duration (Preview sΓΌre) β
β ββ muzibu_hls_timeout (HLS timeout) β
β ββ muzibu_session_polling (Polling interval) β
β ββ muzibu_premium_cache_ttl (Premium cache TTL) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
DOΔRU KULLANIM:
Settings deΔerleri setting() helper ile Γ§ekilmeli.
// DeviceService.php (MEVCUT - DOΔRU)
if (function_exists('setting') && setting('auth_device_limit')) {
return (int) setting('auth_device_limit');
}
// SongStreamController.php (EKLENMELΔ°)
$previewDuration = setting('muzibu_preview_duration')
?? config('muzibu.stream.preview_duration');
// Session Polling (EKLENMELΔ°)
$pollingInterval = setting('muzibu_session_polling')
?? config('muzibu.session.polling_interval');
β οΈ DΔ°KKAT: Γncelik SΔ±rasΔ±
1. Setting deΔeri: setting('key') - Tenant ΓΆzel ayar
2. Config deΔeri: config('muzibu.key') - Uygulama varsayΔ±lanΔ±
3. Fallback: Hardcoded (son Γ§are!)
9. ΓZET: TΓM DΓZELTMELER (Γncelik SΔ±rasΔ±)
π΄ YΓK SEK ΓNCELΔ°K (Kritik Hatalar)
- 1. CSRF Token Auto-Renewal (419 hatasΔ± dΓΌzelt)
- 2. SongStreamController Duplicate Code (DRY ihlali)
- 3. SubscriptionObserver OluΕtur (Premium cache)
- 4. Device Selection Modal Frontend Entegrasyonu
π‘ ORTA ΓNCELΔ°K (Δ°yileΕtirmeler)
- 5. config/muzibu.php OluΕtur (Hardcoded deΔerler)
- 6. Frontend Device Limit Injection (Backend'den al)
- 7. Settings Entegrasyonu (Yeni ayarlar ekle)
- 8. Logout'ta Premium Cache Temizle
π’ DΓΕΓK ΓNCELΔ°K (DokΓΌmantasyon)
- 9. API Response Standardizasyonu
- 10. Loglama Δ°yileΕtirme (Session, Device, Stream)
- 11. Unit Test Yazma (DeviceService, Stream)
π RAPOR TAMAMLANDI
TΓΌm sistem akΔ±ΕlarΔ± analiz edildi β’ Hatalar tespit edildi β’ ΓΓΆzΓΌmler dokΓΌmante edildi
π΅ Muzibu Music Platform β’ Master Plan v1.0 β’ 7 AralΔ±k 2025