diff --git a/app/Http/Controllers/StudyTrackerController.php b/app/Http/Controllers/StudyTrackerController.php index 77f165d8..945df03f 100644 --- a/app/Http/Controllers/StudyTrackerController.php +++ b/app/Http/Controllers/StudyTrackerController.php @@ -130,7 +130,7 @@ public function logTime(Request $request) } $validated = $request->validate([ - 'seconds' => 'required|integer|min:1|max:86400', + 'seconds' => 'required|integer|min:1|max:'.DailyStudyLog::MAX_DAILY_SECONDS, 'date' => 'nullable|date_format:Y-m-d|before_or_equal:today', ]); @@ -146,7 +146,7 @@ public function logTime(Request $request) ] ); - $maxAllowed = max(0, 86400 - (int) $log->total_seconds); + $maxAllowed = max(0, DailyStudyLog::MAX_DAILY_SECONDS - (int) $log->total_seconds); if ($maxAllowed > 0) { $secondsToAdd = min((int) $validated['seconds'], $maxAllowed); $log->increment('total_seconds', $secondsToAdd); diff --git a/app/Models/DailyStudyLog.php b/app/Models/DailyStudyLog.php index c205b85f..4651f66c 100644 --- a/app/Models/DailyStudyLog.php +++ b/app/Models/DailyStudyLog.php @@ -9,6 +9,8 @@ class DailyStudyLog extends Model { + public const MAX_DAILY_SECONDS = 57600; // 16 hours + protected $fillable = [ 'user_id', 'study_date', diff --git a/database/migrations/2026_10_16_124500_drop_excessive_daily_study_logs.php b/database/migrations/2026_10_16_124500_drop_excessive_daily_study_logs.php new file mode 100644 index 00000000..ed039aee --- /dev/null +++ b/database/migrations/2026_10_16_124500_drop_excessive_daily_study_logs.php @@ -0,0 +1,26 @@ +where('total_seconds', '>', 57600) + ->delete(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Deleted invalid excessive logs cannot be reconstructed + } +}; diff --git a/resources/js/components/tracker/StopwatchWidget.vue b/resources/js/components/tracker/StopwatchWidget.vue index 653f77cf..73c54e01 100644 --- a/resources/js/components/tracker/StopwatchWidget.vue +++ b/resources/js/components/tracker/StopwatchWidget.vue @@ -166,14 +166,19 @@ function resetTimer() { localStorage.removeItem(STORAGE_KEY_ACCUMULATED); } +const MAX_DAILY_SECONDS = 57600; // 16 hours max per calendar day + const maxMinutesAllowedForToday = computed(() => { - const remainingSeconds = Math.max(0, 86400 - props.todaySeconds); + const remainingSeconds = Math.max( + 0, + MAX_DAILY_SECONDS - props.todaySeconds, + ); return Math.floor(remainingSeconds / 60); }); const isDayMaxReached = computed(() => { - return props.todaySeconds >= 86400; + return props.todaySeconds >= MAX_DAILY_SECONDS; }); function triggerStopwatchSave() { @@ -187,7 +192,10 @@ function triggerStopwatchSave() { return; } - const remainingAllowed = Math.max(0, 86400 - props.todaySeconds); + const remainingAllowed = Math.max( + 0, + MAX_DAILY_SECONDS - props.todaySeconds, + ); const secondsToLog = Math.min(elapsedSeconds.value, remainingAllowed); if (secondsToLog <= 0) { @@ -212,7 +220,10 @@ function triggerQuickLog(minutes: number) { } const requestedSeconds = minutes * 60; - const remainingAllowed = Math.max(0, 86400 - props.todaySeconds); + const remainingAllowed = Math.max( + 0, + MAX_DAILY_SECONDS - props.todaySeconds, + ); const secondsToLog = Math.min(requestedSeconds, remainingAllowed); if (secondsToLog <= 0) { @@ -639,7 +650,7 @@ const todayFormatted = computed(() => { class="mt-1.5 text-[11px] font-medium text-red-500 dark:text-red-400" > You can log a maximum of - {{ maxMinutesAllowedForToday }} minutes today (24h + {{ maxMinutesAllowedForToday }} minutes today (16h daily limit).

diff --git a/resources/js/pages/Leaderboard/Index.vue b/resources/js/pages/Leaderboard/Index.vue index ae91836f..157c6a5b 100644 --- a/resources/js/pages/Leaderboard/Index.vue +++ b/resources/js/pages/Leaderboard/Index.vue @@ -801,9 +801,18 @@ onBeforeUnmount(() => { class="font-semibold text-slate-800 dark:text-gray-200" >দৈনিক ক্যাপ: - অবাস্তব সময় যোগ রোধ করতে এক ক্যালেন্ডার দিনে সর্বোচ্চ ২৪ + অবাস্তব সময় যোগ রোধ করতে এক ক্যালেন্ডার দিনে সর্বোচ্চ ১৬ ঘণ্টার বেশি লগ করা সম্ভব নয়। +
  • + ফেয়ার-প্লে পলিসি: + অস্বাভাবিক বা সন্দেহজনক কোনো লগিং কার্যকলাপ পরিলক্ষিত + হলে সংশ্লিষ্ট ব্যবহারকারীকে স্বয়ংক্রিয়ভাবে লিডারবোর্ড + র‍্যাঙ্কিং থেকে বাদ দেওয়া হবে। +
  • create(); + + // Rejects request exceeding max daily limit in single call + $this->actingAs($user) + ->post('/tracker/log-time', ['seconds' => DailyStudyLog::MAX_DAILY_SECONDS + 1]) + ->assertSessionHasErrors('seconds'); + + // Allows logging up to max daily limit + $this->actingAs($user) + ->post('/tracker/log-time', ['seconds' => DailyStudyLog::MAX_DAILY_SECONDS]) + ->assertRedirect(); + + $this->assertDatabaseHas('daily_study_logs', [ + 'user_id' => $user->id, + 'study_date' => Carbon::today()->format('Y-m-d'), + 'total_seconds' => DailyStudyLog::MAX_DAILY_SECONDS, + ]); + + // Subsequent logs within the same day are capped at max + $this->actingAs($user) + ->post('/tracker/log-time', ['seconds' => 600]) + ->assertRedirect(); + + $log = DailyStudyLog::where('user_id', $user->id) + ->where('study_date', Carbon::today()->format('Y-m-d')) + ->first(); + + expect($log->total_seconds)->toBe(DailyStudyLog::MAX_DAILY_SECONDS); +}); + test('tracker only includes trackable subjects and trackable nodes flatly', function () { $trackableSubject = Subject::create([ 'name' => 'Trackable Subject',