From d19fb7e11df7d1b3eae5013571bf8dd7b85c631e Mon Sep 17 00:00:00 2001 From: Tajim Date: Tue, 15 Sep 2026 12:40:14 +0600 Subject: [PATCH 1/7] fix(security): restrict unapproved forum posts on user profiles and ban comments from suspended users --- app/Http/Controllers/BlogController.php | 11 ++++- .../Controllers/UserProfileController.php | 18 ++++---- tests/Feature/BlogTest.php | 15 +++++++ tests/Feature/UserProfileTest.php | 45 +++++++++++++++++++ 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php index d458f46a..c5814587 100644 --- a/app/Http/Controllers/BlogController.php +++ b/app/Http/Controllers/BlogController.php @@ -108,7 +108,16 @@ public function toggleReaction(Blog $blog) public function storeComment(Request $request, Blog $blog) { - $userId = auth()->id(); + $user = auth()->user(); + + // Check if user is suspended + if ($user && $user->isBanned()) { + $bannedUntilFormatted = $user->banned_until->diffForHumans(); + + return back()->with('error', "You are temporarily suspended from community participation until {$user->banned_until->toDateTimeString()} ({$bannedUntilFormatted})."); + } + + $userId = $user->id; if ($blog->comments()->where('user_id', $userId)->exists()) { return back()->with('error', 'You have already posted a comment on this blog.'); diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index e1886426..d62ed314 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -39,20 +39,19 @@ public function show(string $username) 'is_verified' => $user->is_verified, ]; - // Appreciations (Received & Given) $appreciationsCount = $user->appreciationsReceived()->count(); $appreciatingCount = $user->appreciationsGiven()->count(); $isAppreciated = auth()->check() ? $user->appreciationsReceived()->where('appreciator_id', auth()->id())->exists() : false; - // Suggested / Discover community members: 2 contributors + 2 general users + // Suggested members: 1 contributors + 3 general users $contributorUsers = User::where('id', '!=', $user->id) ->whereNotNull('username') ->where('is_verified', true) ->select(['id', 'name', 'username', 'institution', 'image_path', 'about', 'is_verified']) ->inRandomOrder() - ->take(2) + ->take(1) ->get(); $excludedIds = $contributorUsers->pluck('id')->push($user->id)->all(); @@ -67,7 +66,6 @@ public function show(string $username) $suggestedUsers = $contributorUsers->concat($randomUsers)->shuffle()->values(); - // Privacy & Lock Evaluation $isOwner = auth()->id() === $user->id; $activityPrivacy = $user->activity_privacy ?? 'public'; $isLocked = false; @@ -123,21 +121,23 @@ public function show(string $username) ]); } - // Forum Contributions - $questionsCount = ForumPost::where('user_id', $user->id)->count(); - $answersCount = ForumAnswer::where('user_id', $user->id)->count(); + $questionsCount = ForumPost::where('user_id', $user->id)->approved()->count(); + $answersCount = ForumAnswer::where('user_id', $user->id) + ->whereHas('post', fn ($q) => $q->approved()) + ->count(); $forumPosts = ForumPost::where('user_id', $user->id) + ->approved() ->with(['subject:id,name,course,slug', 'node:id,name,slug']) ->latest() ->take(5) ->get(); $forumAnswers = ForumAnswer::where('user_id', $user->id) + ->whereHas('post', fn ($q) => $q->approved()) ->with(['post:id,title,slug,curriculum,is_answered']) ->latest() ->take(5) ->get(); - // Contributor Stats & Blogs $publishedBlogs = $user->blogs() ->where('is_published', true) ->withCount(['reactions', 'comments']) @@ -150,6 +150,7 @@ public function show(string $username) // Recent Community Activities $recentForumPosts = ForumPost::where('user_id', $user->id) + ->approved() ->latest('id') ->take(3) ->get() @@ -163,6 +164,7 @@ public function show(string $username) ]); $recentForumAnswers = ForumAnswer::where('user_id', $user->id) + ->whereHas('post', fn ($q) => $q->approved()) ->with('post:id,title,slug') ->latest('id') ->take(3) diff --git a/tests/Feature/BlogTest.php b/tests/Feature/BlogTest.php index 299d88b3..5a6e7442 100644 --- a/tests/Feature/BlogTest.php +++ b/tests/Feature/BlogTest.php @@ -140,3 +140,18 @@ ->has('comments', 1) ); }); + +test('suspended user cannot comment on a blog', function () { + $user = User::factory()->create([ + 'banned_until' => now()->addDays(3), + ]); + $blog = Blog::factory()->create(['is_published' => true]); + + $response = $this->actingAs($user) + ->post("/blogs/{$blog->slug}/comments", [ + 'content' => 'Trying to comment while banned', + ]); + + $response->assertSessionHas('error'); + expect($blog->comments()->count())->toBe(0); +}); diff --git a/tests/Feature/UserProfileTest.php b/tests/Feature/UserProfileTest.php index 590bad22..666d383e 100644 --- a/tests/Feature/UserProfileTest.php +++ b/tests/Feature/UserProfileTest.php @@ -110,6 +110,51 @@ ); }); +test('public profile excludes unapproved forum questions and answers', function () { + $user = User::factory()->create(['username' => 'pending_user']); + $subject = Subject::create([ + 'name' => 'Chemistry', + 'slug' => 'chemistry', + 'course' => 'hsc', + 'tailwind_format' => 'bg-green-500', + 'icon' => 'beaker', + ]); + $node = Node::create([ + 'subject_id' => $subject->id, + 'name' => 'Organic Chemistry', + 'slug' => 'organic-chemistry', + ]); + + $pendingPost = ForumPost::create([ + 'user_id' => $user->id, + 'curriculum' => 'hsc', + 'subject_id' => $subject->id, + 'node_id' => $node->id, + 'title' => 'Pending Question Title', + 'body' => 'Pending post body.', + 'moderation_status' => 'pending', + ]); + + ForumAnswer::create([ + 'forum_post_id' => $pendingPost->id, + 'user_id' => $user->id, + 'body' => 'Answer to pending post', + ]); + + $response = $this->get('/u/pending_user'); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('User/Show') + ->where('stats.questionsCount', 0) + ->where('stats.answersCount', 0) + ->has('forumPosts', 0) + ->has('forumAnswers', 0) + ->has('recentActivities.forum_posts', 0) + ->has('recentActivities.forum_answers', 0) + ); +}); + test('public profile includes both contributors and random users in suggestions', function () { $mainUser = User::factory()->create(['username' => 'current_user']); From f2d5af39761ffd7a42eceeeb4a3fbb56796c7186 Mon Sep 17 00:00:00 2001 From: Tajim Date: Tue, 15 Sep 2026 12:40:16 +0600 Subject: [PATCH 2/7] docs(legal): update privacy, terms, content policy, and contributor agreement terms --- resources/js/pages/legal/ContentPolicy.vue | 28 ++++---- resources/js/pages/legal/PrivacyPolicy.vue | 61 ++++++++++++----- resources/js/pages/legal/TermsConditions.vue | 28 ++++---- resources/js/pages/platform/JoinTeam.vue | 70 ++++++++++---------- 4 files changed, 112 insertions(+), 75 deletions(-) diff --git a/resources/js/pages/legal/ContentPolicy.vue b/resources/js/pages/legal/ContentPolicy.vue index 04e970b9..abfa1556 100644 --- a/resources/js/pages/legal/ContentPolicy.vue +++ b/resources/js/pages/legal/ContentPolicy.vue @@ -146,8 +146,7 @@ import { জমা দিলে (S3/Cloudflare R2-তে আপলোড বা এক্সটার্নাল লিংকের মাধ্যমে), HSCStack সেই কনটেন্ট প্ল্যাটফর্মে সংরক্ষণ, প্রদর্শন এবং শিক্ষার্থীদের কাছে বিতরণের অনুমতি - পায় — তবে কনটেন্টের মূল মালিকানা কন্ট্রিবিউটরের কাছেই - থাকে। + পায়।

@@ -250,15 +249,14 @@ import { উল্লেখ করে সেটি রিপোর্ট করতে পারেন।
  • - Auto-Suspension: কোনো ব্যবহারকারী - ৫টি বা তার বেশি রিপোর্ট পেলে সিস্টেম - স্বয়ংক্রিয়ভাবে তাকে ২৪ ঘণ্টার জন্য চ্যাট থেকে - সাসপেন্ড করবে। + Auto-Suspension: কোনো ব্যবহারকারীর + মেসেজে নির্দিষ্ট সংখ্যক রিপোর্ট পড়লে তাকে + স্বয়ংক্রিয়ভাবে কিছু সময়ের জন্য ব্যান করা হবে ।
  • Message Pruning: প্ল্যাটফর্মের গতি ঠিক রাখতে চ্যাটে রোলিং মেসেজ প্রুনিং ব্যবহার করা - হয়, যেখানে সর্বশেষ প্রায় ২০০টি মেসেজ সংরক্ষিত + হয়, যেখানে সর্বশেষ কিছু মেসেজ(যেমন: ২০০) সংরক্ষিত থাকে।
  • @@ -309,10 +307,9 @@ import {
  • ছবি ও ডায়াগ্রাম সংযুক্তি: প্রশ্নের সাথে প্রাসঙ্গিক ডায়াগ্রাম বা বইয়ের পৃষ্ঠার স্পষ্ট - ছবি সংযুক্ত করা যাবে (সর্বোচ্চ ৫ মেগাবাইট)। - কপিরাইটযুক্ত কোনো গোপনীয় নথি, পরীক্ষার প্রশ্নফাঁস - সংক্রান্ত উপাদান বা আপত্তিকর ছবি আপলোড করা সম্পূর্ণ - নিষিদ্ধ। + ছবি সংযুক্ত করা যাবে । কপিরাইটযুক্ত কোনো গোপনীয় নথি, + পরীক্ষার প্রশ্নফাঁস সংক্রান্ত উপাদান বা আপত্তিকর ছবি + আপলোড করা সম্পূর্ণ নিষিদ্ধ।
  • ভোট ও কমিউনিটি রেটিং: ফোরামের আপভোট @@ -536,14 +533,19 @@ import {
  • Appreciations: প্রোফাইলে অন্যদের - প্রশংসা (appreciations) প্রদান করার ক্ষেত্রে - সম্মানজনক ভাষা ব্যবহার করতে হবে। + Appreciate করা যাবে ।
  • Profile Content: ব্যবহারকারীর প্রোফাইলের কনটেন্ট এবং কার্যকলাপের সম্পূর্ণ দায়ভার ওই ব্যবহারকারীর।
  • +
  • + Poking: ব্যবহারকারীর একে অন্যকে + নির্দিষ্ট কিছু বন্ধুত্বপূর্ণ মেসেজের মাধ্যমে Poke + করতে পারেন, কোনো ব্যবহারকারী চাইলে তার প্রোফাইল হতে + এই অপশনটি বন্ধ করে রাখতে পারেন। +
  • diff --git a/resources/js/pages/legal/PrivacyPolicy.vue b/resources/js/pages/legal/PrivacyPolicy.vue index 1dbc5da6..e6e6c160 100644 --- a/resources/js/pages/legal/PrivacyPolicy.vue +++ b/resources/js/pages/legal/PrivacyPolicy.vue @@ -11,6 +11,7 @@ import { PersonStanding, Mail, MessageSquare, + RefreshCw, } from 'lucide-vue-next'; @@ -124,8 +125,9 @@ import { Session Cookies এবং CSRF টোকেন যা সিকিউরিটির জন্য জরুরি। পাশাপাশি আপনার সিলেক্ট করা কোর্সের জন্য preferred_course কুকি এবং আমাদের PWA অ্যাপের জন্য Service Worker ব্যবহার - করা হয়। আমরা কোনো ধরনের থার্ড-পার্টি অ্যাডভার্টাইজিং - কুকিজ ব্যবহার করি না। + করা হয়। ভবিষ্যতে প্ল্যাটফর্মে কোনো স্ট্যান্ডার্ড + অ্যানালিটিক্স বা বিজ্ঞাপন নেটওয়ার্ক ব্যবহৃত হলে তাদের + নিজস্ব কুকি পলিসি প্রযোজ্য হতে পারে।

    @@ -164,7 +166,7 @@ import {
  • অন্যান্য: Short.io (লিংক শর্টেনার), YouTube (এম্বেড করা ভিডিও) এবং নির্ভরযোগ্য ইমেইল - প্রোভাইডার (Postmark/Resend/SES)। + প্রোভাইডার (Brevo, Resend বা অনুরূপ)।
  • @@ -187,12 +189,12 @@ import { class="mt-1.5 text-sm leading-relaxed font-medium text-slate-500 dark:text-gray-400" > আপনার ডেটার সর্বোচ্চ নিরাপত্তা নিশ্চিত করতে আমরা HTTPS - এনক্রিপশন, সিকিউর ক্লাউড স্টোরেজ, মজবুত পাসওয়ার্ড - হ্যাশিং এবং প্রোডাকশন লেভেলের সেফগার্ড ব্যবহার করি। - স্প্যাম রোধে রয়েছে রেট লিমিটিং ব্যবস্থা। এছাড়া ডেটা লস - এড়াতে আমাদের ডাটাবেস এবং ফাইলগুলো স্বয়ংক্রিয়ভাবে - Google Drive-এ ব্যাকআপ নেওয়া হয়। তবে অনলাইনে কোনো - সিস্টেমই ১০০% সুরক্ষিত নয়। + এনক্রিপশন, সিকিউর ক্লাউড স্টোরেজ এবং প্রোডাকশন লেভেলের + সেফগার্ড ব্যবহার করি। স্প্যাম রোধে রয়েছে রেট লিমিটিং + ব্যবস্থা। এছাড়া ডেটা লস এড়াতে আমাদের ডাটাবেস এবং + ফাইলগুলো স্বয়ংক্রিয়ভাবে Google Drive-এ ব্যাকআপ নেওয়া + হয়। তবে মনে রাখতে হবে অনলাইনে কোনো কিছুই শতভাগ সুরক্ষিত + নয় ।

    @@ -245,11 +247,12 @@ import { class="mt-1.5 text-sm leading-relaxed font-medium text-slate-500 dark:text-gray-400" > প্ল্যাটফর্ম উন্নত করার উদ্দেশ্যে আমরা মূলত PostHog - ব্যবহার করি যা pageviews, unique visitors, active users - এবং referrers সম্পর্কে ধারণা দেয়। পাশাপাশি Cloudflare ও - আমাদের ইন্টার্নাল অ্যানালিটিক্স কাজ করে। এসব ডেটা - সম্পূর্ণ রূপে non-personal এবং শুধু এগ্রিগেট আকারে - ব্যবহৃত হয়। + ব্যবহার করি যা pageviews, unique visitors, active users, + referrers ইত্যাদি(third party analytics এর সকল + স্ট্যান্ডার্ড সার্ভিসেস) বিষয় সম্পর্কে ধারণা দেয়। + পাশাপাশি Cloudflare ও আমাদের ইন্টার্নাল অ্যানালিটিক্স + কাজ করে। এসব ডেটা সম্পূর্ণ রূপে non-personal এবং শুধু + এগ্রিগেট আকারে ব্যবহৃত হয়।

    @@ -274,8 +277,8 @@ import { ইমেইল, নির্দিষ্ট মাইলস্টোন নোটিফিকেশন, ব্লগে কমেন্ট অ্যালার্ট এবং গুরুত্বপূর্ণ ঘোষণা (bulk announcements) পাঠিয়ে থাকি। আপনি চাইলে যেকোনো সময় আপনার অ্যাকাউন্ট - সেটিংস বা ইমেইলের নিচে থাকা আনসাবস্ক্রাইব লিংকে ক্লিক - করে এসব ইমেইল বন্ধ করতে পারেন। + সেটিংস এ ক্লিক করে অথবা Support Center থেকে এসব ইমেইল + বন্ধ(unsubscribe) করতে পারেন।

    @@ -362,6 +365,32 @@ import {

    + + +
    +
    + +
    +
    +

    + ১১. ব্যবসায়িক হস্তান্তর ও মালিকানা পরিবর্তন +

    +

    + ভবিষ্যতে HSCStack-এর প্রাতিষ্ঠানিক রূপান্তর, একীভূতকরণ + (merger), অধিগ্রহণ (acquisition), বা প্ল্যাটফর্মের + মালিকানা তৃতীয় পক্ষের কাছে হস্তান্তর করা হলে, + প্ল্যাটফর্মের নিরবচ্ছিন্ন সেবা প্রদান অব্যাহত রাখার + স্বার্থে সংরক্ষিত ইউজার অ্যাকাউন্ট ও ডেটা নতুন পরিচালনা + কর্তৃপক্ষের কাছে স্থানান্তরিত হতে পারে। +

    +
    +
    diff --git a/resources/js/pages/legal/TermsConditions.vue b/resources/js/pages/legal/TermsConditions.vue index 9253c772..532781c2 100644 --- a/resources/js/pages/legal/TermsConditions.vue +++ b/resources/js/pages/legal/TermsConditions.vue @@ -234,17 +234,21 @@ import {

    - ৬. Intellectual Property ও Fair Use + ৬. Intellectual Property, ভলান্টিয়ার নীতিমালা ও Fair + Use

    আমাদের original টেক্সট লেআউট, সিস্টেম স্ক্রিপ্ট, কোড এবং - নিজস্ব ব্লগ আর্টিকেলের মালিকানা আমাদের। কনট্রিবিউটরদের - জমা দেওয়া নোট, ব্লগ বা অন্যান্য কনটেন্টের মূল মালিকানা - সংশ্লিষ্ট কনট্রিবিউটরের কাছেই থাকে — তবে তারা - HSCStack-কে এগুলো সংরক্ষণ, প্রদর্শন ও বিতরণের একটি - লাইসেন্স প্রদান করেন, যেমনটি আমাদের + নিজস্ব আর্টিকেলের স্বত্বাধিকার প্ল্যাটফর্মের। ক্যাম্পাস + অ্যাম্বাসেডর, মডারেটর, কিউরেটর বা ডেভেলপার হিসেবে টিমে + যোগদান ও কন্ট্রিবিউশন সম্পূর্ণ অবৈতনিক এবং স্বেচ্ছাসেবী + (100% voluntary)। এই ভূমিকাগুলোর সাথে কোনো প্রকার আর্থিক + পারিশ্রমিক, কর্মসংস্থান বা অন্য কোনো দাবি অন্তর্ভুক্ত + নয়। কনট্রিবিউটরদের শেয়ারকৃত রিসোর্স অবাণিজ্যিক ও + উন্মুক্তভাবে প্রদর্শন ও সংরক্ষণের জন্য HSCStack লাইসেন্স + পায়, যেমনটি আমাদের Content Policy, অথবা প্ল্যাটফর্মের নিরাপত্তা বিঘ্নিত করলে মডারেটর বা - অ্যাডমিন যেকোনো অ্যাকাউন্ট (ছাত্র বা কনট্রিবিউটর) স্থগিত - বা বাতিল করার অধিকার রাখেন। লাইভ চ্যাটে স্বয়ংক্রিয় - ব্যান ছাড়াও মডারেটররা ম্যানুয়ালি ব্যবস্থা নিতে পারেন। - গুরুতর নিয়মভঙ্গ, স্প্যামিং বা কপিরাইট লঙ্ঘনের ক্ষেত্রে - কোনো পূর্ব নোটিশ ছাড়াই কন্টেন্ট রিমুভ বা অ্যাকাউন্ট - স্থায়ীভাবে ডিলিট করা হতে পারে। + অ্যাডমিন যেকোনো অ্যাকাউন্ট (শিক্ষার্থী বা কনট্রিবিউটর) + স্থগিত বা বাতিল করার অধিকার রাখেন। লাইভ চ্যাটে + স্বয়ংক্রিয় ব্যান ছাড়াও মডারেটররা ম্যানুয়ালি ব্যবস্থা + নিতে পারেন। গুরুতর নিয়মভঙ্গ, স্প্যামিং বা কপিরাইট + লঙ্ঘনের ক্ষেত্রে কোনো পূর্ব নোটিশ ছাড়াই কন্টেন্ট রিমুভ + বা অ্যাকাউন্ট স্থায়ীভাবে ডিলিট করা হতে পারে।

    diff --git a/resources/js/pages/platform/JoinTeam.vue b/resources/js/pages/platform/JoinTeam.vue index be9abd33..df2624ab 100644 --- a/resources/js/pages/platform/JoinTeam.vue +++ b/resources/js/pages/platform/JoinTeam.vue @@ -45,9 +45,8 @@ const roles: Role[] = [ description: 'হাই-কোয়ালিটি পড়ার ম্যাটেরিয়াল, চ্যাপ্টার-ওয়াইজ নোটস ও প্রশ্নব্যাংক গুছিয়ে রাখা আপনার কাজ।', responsibilities: [ - 'নিখুঁত বোর্ড প্রশ্ন, মডেল টেস্ট ও সেরা হ্যান্ডনোট সংগ্রহ করা', + 'নিখুঁত বোর্ড প্রশ্ন, প্রাক্টিক্যাল ও সেরা হ্যান্ডনোট সংগ্রহ করা', 'চ্যাপ্টার ও বিষয় অনুযায়ী রিসোর্স নিখুঁতভাবে সাজানো ও রিচেক করা', - 'প্র্যাক্টিক্যাল রেকর্ডস ও টেস্ট পেপার ডেটা গুছিয়ে রাখা', ], icon: FolderHeart, badgeStyle: @@ -64,7 +63,6 @@ const roles: Role[] = [ responsibilities: [ 'HSC/SSC শিক্ষার্থীদের পড়াশোনার কার্যকরী টিপস ও গাইডলাইন লেখা', 'সহজ ও প্রাঞ্জল ভাষায় গুরুত্বপূর্ণ বিষয় বিশ্লেষণ করা', - 'অফিশিয়াল ব্লগ সেকশনে নিয়মিত কোয়ালিটি কন্টেন্ট প্রকাশ করা', ], icon: PenTool, badgeStyle: @@ -80,7 +78,6 @@ const roles: Role[] = [ 'স্টুডেন্ট সাপোর্ট টিকেট পরিচালনা, সমস্যার দ্রুত সমাধান ও অভ্যন্তরীণ প্ল্যাটফর্ম সহায়তা দেওয়া।', responsibilities: [ 'শিক্ষার্থীদের জমা দেওয়া সাপোর্ট টিকেট ও ইস্যু রিভিউ করে সমাধান দেওয়া', - 'কোনো রিসোর্স বা লিংকে সমস্যা থাকলে তা দ্রুত রিপোর্ট ও ফিক্স নিশ্চিত করা', 'ইউজার এক্সপেরিয়েন্স নিরবচ্ছিন্ন রাখতে কোর টিমের সাথে সরাসরি কাজ করা', ], icon: LifeBuoy, @@ -96,7 +93,6 @@ const roles: Role[] = [ description: 'লাইভ গ্লোবাল চ্যাটে স্টুডেন্টদের সহায়তা করা, স্প্যামিং রোধ ও ফ্রেন্ডলি পরিবেশ রক্ষা করা।', responsibilities: [ - 'গ্লোবাল চ্যাটে শিক্ষার্থীদের প্রশ্ন ও আলোচনায় আন্তরিক সহায়তা দেওয়া', 'স্প্যামিং বা আচরণবিধি বিরোধী মেসেজ দ্রুত মডারেট করা', 'ইতিবাচক, প্রাণবন্ত ও শিক্ষার্থীবান্ধব আলোচনা বজায় রাখা', ], @@ -115,7 +111,6 @@ const roles: Role[] = [ responsibilities: [ 'ফোরামের প্রশ্ন ও উত্তর পর্যালোচনা করে সঠিক ও মানসম্মত একাডেমিক আলোচনা নিশ্চিত করা', 'অপ্রাসঙ্গিক, ক্ষতিকর বা স্প্যাম পোস্ট ও কমেন্ট দ্রুত মডারেট এবং রিপোর্ট রিভিউ করা', - 'কঠিন প্রশ্নের সমাধানে শিক্ষার্থীদের সহায়তা ও সক্রিয় অংশগ্রহণ উৎসাহিত করা', ], icon: MessageSquareText, badgeStyle: @@ -132,7 +127,6 @@ const roles: Role[] = [ responsibilities: [ 'ফেসবুক গ্রুপ ও পেজের মেসেজ এবং কমেন্টে দ্রুত সহায়তা দেওয়া', 'কমিউনিটিতে তথ্যবহুল পোস্ট ও গুরুত্বপূর্ণ নোটিশ প্রচার করা', - 'গ্রুপের পোস্ট মডারেশন ও স্প্যামিং রোধে সক্রিয় থাকা', ], icon: Share2, badgeStyle: @@ -147,8 +141,7 @@ const roles: Role[] = [ description: 'আমাদের এই উন্মুক্ত শিক্ষা প্ল্যাটফর্মকে একদম তৃণমূল পর্যায়ের শিক্ষার্থীদের কাছে পৌঁছে দিতে সাহায্য করুন।', responsibilities: [ - 'বিভিন্ন college ও স্টুডেন্ট গ্রুপে প্ল্যাটফর্মের ফ্রি রিসোর্স ও আপডেট শেয়ার করা', - 'নিজের কলেজে HSCStack-এর প্রতিনিধি হিসেবে শিক্ষার্থীদের গাইড করা', + 'নিজের স্কুল কিংবা কলেজে HSCStack-এর প্রতিনিধি হিসেবে ফ্রি রিসোর্স ও আপডেট শেয়ার করা', 'স্টুডেন্টদের প্রয়োজনীয় স্টাডি মেটেরিয়ালের চাহিদা টিমে জানানো', ], icon: Megaphone, @@ -164,9 +157,8 @@ const roles: Role[] = [ description: 'আমাদের ওপেন সোর্স প্ল্যাটফর্ম আর্কিটেকচার, পারফরম্যান্স ও UI কম্পোনেন্ট উন্নত করতে কাজ করবেন।', responsibilities: [ - 'সুপার-ফাস্ট Vue / Inertia / Tailwind ফ্রন্টএন্ড ফিচার তৈরি করা', - 'মোবাইল ফ্রেন্ডলিনেস ও রেস্পনসিভ এক্সপেরিয়েন্স সর্বোচ্চ মানে রাখা', - 'নতুন ফিচার ও ডেটাবেজ অপ্টিমাইজেশনে সরাসরি অবদান রাখা', + 'Vue3 & Laravel এ নতুন ফিচার ডেভেলপ করা', + 'বাগ ফিক্সিং, পারফরমেন্স ইম্প্রুভ করা & সিকিউরিটি অডিট করা', ], icon: Code2, badgeStyle: @@ -506,31 +498,20 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown)); - +
    -
    -

    - Applying for multiple roles or have an idea? -

    -

    - একসাথে একাধিক রোলে কাজ করতে চাইলে বা অন্য কোনো স্পেশাল - আইডিয়া নিয়ে অবদান রাখতে সরাসরি আবেদন করুন। -

    -
    - - + 💡 + নোট: + HSCStack-এর সকল টিম রোল (অ্যাম্বাসেডর, মডারেটর, কিউরেটর ও + ডেভেলপার) সম্পূর্ণ অবৈতনিক এবং ১০০% স্বেচ্ছাসেবী (100% + voluntary)। এর সাথে কোনো আর্থিক পারিশ্রমিক বা অন্য কোনো দাবি + অন্তর্ভুক্ত নয়। +

    @@ -673,6 +654,27 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown)); > + +
    + আবেদন জমা দেওয়ার মাধ্যমে আপনি নিশ্চিত করছেন যে এটি + সম্পূর্ণ অবৈতনিক ও স্বেচ্ছাসেবী ভূমিকা এবং আপনি আমাদের + Terms of Service + ও + Content Policy-তে সম্মত। +
    +
    + + +
    - - -
    - -

    - Playlists with multiple pages will be automatically - fetched and created as sequential video resources. -

    -
    diff --git a/resources/js/components/navigation/Navigation.tsx b/resources/js/components/navigation/Navigation.tsx index a6b06620..9ca53043 100644 --- a/resources/js/components/navigation/Navigation.tsx +++ b/resources/js/components/navigation/Navigation.tsx @@ -491,7 +491,7 @@ export const SiteRail = defineComponent({ }`} /> - About Us + Our Contributors {isActive('/about-us') && ( @@ -1198,7 +1198,7 @@ export const SiteDrawer = defineComponent({ }`} /> - About Us + Our Contributors {isActive('/about-us') && ( diff --git a/resources/js/pages/Blog/Show.vue b/resources/js/pages/Blog/Show.vue index 61cb3c41..82e813be 100644 --- a/resources/js/pages/Blog/Show.vue +++ b/resources/js/pages/Blog/Show.vue @@ -455,7 +455,7 @@ const formatTimeAgo = (dateStr: string) => { - Want to write study guides or articles on HSCStack? + Want to write articles on HSCStack? @@ -699,7 +699,7 @@ const formatTimeAgo = (dateStr: string) => { ? `/u/${blog.user.username}` : '#' " - class="text-lg font-semibold text-slate-900 underline transition dark:text-gray-100" + class="text-lg font-semibold text-slate-900 transition dark:text-gray-100" > {{ blog.user?.name }} diff --git a/resources/js/pages/Chat/Index.vue b/resources/js/pages/Chat/Index.vue index a97a7538..bde6cb12 100644 --- a/resources/js/pages/Chat/Index.vue +++ b/resources/js/pages/Chat/Index.vue @@ -19,7 +19,6 @@ import { MoreHorizontal, Radio, AtSign, - LifeBuoy, Users, } from 'lucide-vue-next'; import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; @@ -1529,8 +1528,7 @@ onUnmounted(() => {

    - অন্যান্য শিক্ষার্থীদের সাথে সরাসরি কথা বলুন ও প্রশ্ন শেয়ার - করুন। + অন্যান্য শিক্ষার্থীদের সাথে সরাসরি কথা বলুন

    @@ -2691,8 +2689,8 @@ onUnmounted(() => {

    - সবার জন্য চ্যাট নিরাপদ ও ফ্রেন্ডলি রাখতে - নিচের নিয়মগুলো মেনে চলুন। + সবার জন্য চ্যাট নিরাপদ রাখতে নিচের নিয়মগুলো + মেনে চলুন।

    @@ -2748,7 +2746,6 @@ onUnmounted(() => { > বাংলা, ইংরেজি বা বাংলিশ কোনো ভাষাতেই গালাগালি বা অশালীন শব্দ ব্যবহার করা যাবে না। - এমন মেসেজ অটোমেটিক ব্লক হবে।

    @@ -2809,45 +2806,15 @@ onUnmounted(() => { অটো-ব্যান ও এনফোর্সমেন্ট পলিসি:

    - একটি মেসেজে ৫ জন শিক্ষার্থীর রিপোর্ট (৫ Reports) + একটি মেসেজে নির্দিষ্ট সংখ্যক শিক্ষার্থীর রিপোর্ট পড়লে সংশ্লিষ্ট ব্যবহারকারী স্বয়ংক্রিয়ভাবে ১ দিনের জন্য চ্যাট - ব্যানস্বয়ংক্রিয়ভাবে কিছু সময়ের জন্য ব্যান হবেন। এছাড়া নিয়ম ভঙ্গে মডারেটররা তাৎক্ষণিক স্থায়ী ব্যান দিতে পারেন।

    - - -
    -
    - - কোনো টেকনিক্যাল সমস্যা বা সহায়তার জন্য? -
    - - সাপোর্ট সেন্টার → - -
    - - -
    - -
    diff --git a/resources/js/pages/Node.vue b/resources/js/pages/Node.vue index 21994123..af11e92e 100644 --- a/resources/js/pages/Node.vue +++ b/resources/js/pages/Node.vue @@ -68,10 +68,8 @@ const totalItemsCount = computed( () => (props.nodes?.length ?? 0) + (props.resources?.length ?? 0), ); -// Modals const showUpvotersModal = ref(false); -// Optimistic Vote State const localUserVote = ref(props.userVote); const localUpvotesCount = ref(props.upvotesCount); const localDownvotesCount = ref(props.downvotesCount); @@ -242,7 +240,7 @@ const handleVote = (type: 'up' | 'down') => { class="mt-1.5 text-sm font-semibold text-slate-400 dark:text-gray-500" > {{ parentTitle }} · - {{ totalItemsCount }} Items Total + {{ totalItemsCount }} Folders Total

    diff --git a/resources/js/pages/Profile.vue b/resources/js/pages/Profile.vue index 3b02bd13..ed33d26f 100644 --- a/resources/js/pages/Profile.vue +++ b/resources/js/pages/Profile.vue @@ -326,13 +326,13 @@ const submitForm = () => { for="institution" class="mb-1.5 block text-xs font-semibold text-slate-700 dark:text-gray-300" > - Institution / Organization + Institution - আপনার পড়ালেখার অগ্রগতি, ফোরাম প্রশ্ন, উত্তর এবং - আর্টিকেল কারা দেখতে পারবে তা নির্ধারণ করুন। + আপনার প্রোফাইলে ফোরাম প্রশ্ন, উত্তর, পড়ালেখার + অগ্রগতি এবং আর্টিকেল কারা দেখতে পারবে তা নির্ধারণ + করুন।

    @@ -921,11 +922,11 @@ const submitForm = () => {

    - Need Help or Account Support? + Account Deletion & Support.

    - Need help with your account or deletion? Open a - support ticket. + অ্যাকাউন্ট ডিলেট করতে চান কিংবা অন্য কোনো সমস্যা? + সরাসরি সাপোর্টে কথা বলুন !

    diff --git a/resources/js/pages/Projects.vue b/resources/js/pages/Projects.vue index 53085ca3..b6520eea 100644 --- a/resources/js/pages/Projects.vue +++ b/resources/js/pages/Projects.vue @@ -64,7 +64,7 @@ import { ExternalLink, ArrowRight, Users } from 'lucide-vue-next'; class="inline-flex items-center gap-1.5 rounded-lg bg-indigo-50 px-2.5 py-1 text-xs font-bold text-indigo-700 dark:bg-indigo-500/10 dark:text-indigo-300" > - 1000+ Users + 640+ Users
    @@ -109,13 +109,13 @@ import { ExternalLink, ArrowRight, Users } from 'lucide-vue-next';

    - ResultRank + ResultStack

    - 2500+ Users + 6600+ Users @@ -132,12 +132,12 @@ import { ExternalLink, ArrowRight, Users } from 'lucide-vue-next'; class="mt-6 border-t border-slate-100 pt-4 dark:border-gray-800" > - Visit ResultRank + Visit ResultStack diff --git a/resources/js/pages/Resource.vue b/resources/js/pages/Resource.vue index 42666a51..aadff059 100644 --- a/resources/js/pages/Resource.vue +++ b/resources/js/pages/Resource.vue @@ -91,11 +91,7 @@ watch( ); const handleToggleComplete = () => { - if ( - !requireAuth( - 'Please sign in to mark study materials as completed and track your syllabus progress.', - ) - ) { + if (!requireAuth('Please sign in to mark study materials as complete.')) { return; } @@ -157,11 +153,7 @@ watch( ); const handleDownload = () => { - if ( - !requireAuth( - 'Please sign in to download full-resolution study materials.', - ) - ) { + if (!requireAuth('Please sign in to download this resource.')) { return; } diff --git a/resources/js/pages/Support.vue b/resources/js/pages/Support.vue index abeb82c7..a18bfb7e 100644 --- a/resources/js/pages/Support.vue +++ b/resources/js/pages/Support.vue @@ -302,7 +302,7 @@ const submitTicket = () => { id="subject" v-model="form.subject" type="text" - placeholder="সংক্ষেপে সমস্যার মূল কথা লিখুন (যেমন: পদার্থবিজ্ঞান অধ্যায় ২ নোট ওপেন হচ্ছে না)" + placeholder="সংক্ষেপে সমস্যার মূল কথা লিখুন (যেমন: অ্যাকাউন্টে লগইন হচ্ছে না )" required class="w-full rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-xs text-slate-900 placeholder:text-slate-400 focus:border-indigo-600 focus:ring-2 focus:ring-indigo-600/20 focus:outline-none dark:border-gray-800 dark:bg-gray-950 dark:text-gray-100 dark:placeholder:text-gray-500" /> diff --git a/resources/js/pages/User/Show.vue b/resources/js/pages/User/Show.vue index 866143f4..8543fd1d 100644 --- a/resources/js/pages/User/Show.vue +++ b/resources/js/pages/User/Show.vue @@ -346,6 +346,7 @@ const handleAppreciate = () => { { preserveScroll: true, preserveState: !props.isLocked, + only: ['auth', 'flash'], onError: () => { localIsAppreciated.value = props.isAppreciated; localAppreciationsCount.value = props.appreciationsCount; diff --git a/resources/js/pages/admin/EmailSend.vue b/resources/js/pages/admin/EmailSend.vue index 432b8e1c..022f4313 100644 --- a/resources/js/pages/admin/EmailSend.vue +++ b/resources/js/pages/admin/EmailSend.vue @@ -735,7 +735,7 @@ const submitForm = () => { - {{ appName }} <team@hscstack.com> + {{ appName }} <team@example.com>
    diff --git a/resources/js/pages/ai/Index.vue b/resources/js/pages/ai/Index.vue index 7259bff1..b2a67d05 100644 --- a/resources/js/pages/ai/Index.vue +++ b/resources/js/pages/ai/Index.vue @@ -27,13 +27,7 @@ const mockSubjects = [ id: 'physics', name: 'Physics', icon: Atom, - topics: [ - 'Vector', - 'Dynamics', - 'Work, Energy & Power', - 'Gravitation', - 'Thermodynamics', - ], + topics: ['Vector', 'Work, Energy & Power', 'Gravitation'], }, { id: 'chemistry', @@ -43,19 +37,13 @@ const mockSubjects = [ 'Qualitative Chemistry', 'Periodic Properties', 'Chemical Change', - 'Electrochemistry', ], }, { id: 'math', name: 'Higher Math', icon: Sigma, - topics: [ - 'Matrices & Determinants', - 'Straight Lines', - 'Circles', - 'Calculus (Differentiation)', - ], + topics: ['Matrices & Determinants', 'Straight Lines', 'Circles'], }, ]; diff --git a/resources/js/pages/auth/Login.vue b/resources/js/pages/auth/Login.vue index 464385b8..2eb62bc3 100644 --- a/resources/js/pages/auth/Login.vue +++ b/resources/js/pages/auth/Login.vue @@ -4,7 +4,6 @@ import { LogIn, ShieldCheck, Zap, - KeyRound, UserCheck, AlertCircle, } from 'lucide-vue-next'; @@ -133,24 +132,6 @@ const googleAuthUrl = redirectParam
    -
    -
    - -
    -
    - Zero Password : - কোনো পাসওয়ার্ড মনে রাখা বা রিসেট করার ঝামেলা - নেই—সবকিছু Google-এর মাধ্যমে secure। -
    -
    -
    Be a Contributor: - একাউন্ট খোলার পর আপনি আমাদের টিমে Contributor বা - Curator হিসেবে + একাউন্ট খোলার পর আপনি আমাদের টিমে Contributor + হিসেবে - Apply করতে পারবেন । + Apply করতে পারবেন +
    From 6ab9e93fa75a66f840ce1b2f788504bc116027d9 Mon Sep 17 00:00:00 2001 From: Tajim Date: Tue, 15 Sep 2026 12:44:57 +0600 Subject: [PATCH 5/7] fix(auth,blog): require receive_emails in onboarding and simplify suspended user comment error --- app/Http/Controllers/AuthController.php | 11 +++++------ app/Http/Controllers/BlogController.php | 4 +--- tests/Feature/AuthenticationTest.php | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index aac3bb57..d234521f 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -156,7 +156,7 @@ public function completeOnboarding(Request $request) ], 'school' => ['required', 'string', 'max:255', new CleanText], 'image' => ['sometimes', 'nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'], - 'receive_emails' => ['sometimes', 'boolean'], + 'receive_emails' => ['required', 'boolean'], 'appreciations' => ['nullable', 'array', 'max:4'], 'appreciations.*' => ['integer', 'distinct', 'exists:users,id'], ], [ @@ -179,10 +179,6 @@ public function completeOnboarding(Request $request) ->first(); if (! $user) { - $receiveEmails = $request->has('receive_emails') - ? $request->boolean('receive_emails') - : true; - $user = User::create([ 'name' => $validated['name'], 'username' => $validated['username'], @@ -190,12 +186,15 @@ public function completeOnboarding(Request $request) 'google_id' => $onboardingData['google_id'], 'institution' => $validated['school'], 'image_path' => $imagePath, - 'receive_emails' => $receiveEmails, + 'receive_emails' => $validated['receive_emails'], 'email_verified_at' => now(), ]); $user->notify(new WelcomeNotification); } else { + $user->receive_emails = $validated['receive_emails']; + $user->save(); + if ($imagePath) { if ($user->image_path) { Storage::delete($user->image_path); diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php index c5814587..7ff4b9d2 100644 --- a/app/Http/Controllers/BlogController.php +++ b/app/Http/Controllers/BlogController.php @@ -112,9 +112,7 @@ public function storeComment(Request $request, Blog $blog) // Check if user is suspended if ($user && $user->isBanned()) { - $bannedUntilFormatted = $user->banned_until->diffForHumans(); - - return back()->with('error', "You are temporarily suspended from community participation until {$user->banned_until->toDateTimeString()} ({$bannedUntilFormatted})."); + return back()->with('error', 'You are temporarily suspended from community participation.'); } $userId = $user->id; diff --git a/tests/Feature/AuthenticationTest.php b/tests/Feature/AuthenticationTest.php index 387f417b..952c47bb 100644 --- a/tests/Feature/AuthenticationTest.php +++ b/tests/Feature/AuthenticationTest.php @@ -97,6 +97,7 @@ 'name' => 'Custom Name', 'username' => 'custom_handle', 'school' => 'Notre Dame College', + 'receive_emails' => true, ]); $user = User::where('email', 'newuser@example.com')->first(); @@ -105,6 +106,7 @@ $this->assertEquals('custom_handle', $user->username); $this->assertEquals('Notre Dame College', $user->institution); $this->assertEquals('google-id-12345', $user->google_id); + $this->assertTrue($user->receive_emails); $this->assertNotNull($user->email_verified_at); $this->assertAuthenticatedAs($user); @@ -152,6 +154,7 @@ 'username' => 'photo_user', 'school' => 'Dhaka College', 'image' => $file, + 'receive_emails' => true, ]); $user = User::where('email', 'photo_user@example.com')->first(); @@ -180,6 +183,7 @@ 'name' => 'Google Avatar User', 'username' => 'google_avatar_user', 'school' => 'Rajshahi College', + 'receive_emails' => true, ]); $user = User::where('email', 'google_avatar@example.com')->first(); @@ -251,6 +255,7 @@ 'name' => 'New Redirect User', 'username' => 'new_redirect_user', 'school' => 'Dhaka College', + 'receive_emails' => true, ]); $onboardResponse->assertRedirect(url('/ai')); @@ -278,6 +283,7 @@ 'name' => 'New Subdomain User', 'username' => 'new_subdomain_user', 'school' => 'Rajshahi College', + 'receive_emails' => true, ]); $onboardResponse->assertStatus(409); @@ -429,6 +435,7 @@ 'name' => 'Fan User', 'username' => 'fan_user', 'school' => 'Notre Dame College', + 'receive_emails' => true, 'appreciations' => [$contributor1->id, $contributor2->id], ]); @@ -448,3 +455,21 @@ $this->assertAuthenticatedAs($newUser); $response->assertRedirect(route('user.profile', 'fan_user')); }); + +test('completing onboarding requires receive_emails field', function () { + $response = $this->withSession([ + 'onboarding_user' => [ + 'google_id' => 'google-id-req-email', + 'email' => 'reqemail@example.com', + 'name' => 'Req Email User', + 'avatar' => null, + ], + ])->post(route('onboarding.complete'), [ + 'name' => 'Req Email User', + 'username' => 'req_email_user', + 'school' => 'Notre Dame College', + ]); + + $response->assertSessionHasErrors(['receive_emails']); + $this->assertGuest(); +}); From c4426ff26c37ebfb4915686d4b6c8106dc0ec587 Mon Sep 17 00:00:00 2001 From: Tajim Date: Tue, 15 Sep 2026 12:51:13 +0600 Subject: [PATCH 6/7] refactor(auth): simplify user creation during onboarding completion --- app/Http/Controllers/AuthController.php | 38 +++++++------------------ 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index d234521f..9599848f 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -174,34 +174,18 @@ public function completeOnboarding(Request $request) $imagePath = $this->downloadGoogleAvatar($onboardingData['avatar']); } - $user = User::where('google_id', $onboardingData['google_id']) - ->orWhere('email', $onboardingData['email']) - ->first(); - - if (! $user) { - $user = User::create([ - 'name' => $validated['name'], - 'username' => $validated['username'], - 'email' => $onboardingData['email'], - 'google_id' => $onboardingData['google_id'], - 'institution' => $validated['school'], - 'image_path' => $imagePath, - 'receive_emails' => $validated['receive_emails'], - 'email_verified_at' => now(), - ]); - - $user->notify(new WelcomeNotification); - } else { - $user->receive_emails = $validated['receive_emails']; - $user->save(); + $user = User::create([ + 'name' => $validated['name'], + 'username' => $validated['username'], + 'email' => $onboardingData['email'], + 'google_id' => $onboardingData['google_id'], + 'institution' => $validated['school'], + 'image_path' => $imagePath, + 'receive_emails' => $validated['receive_emails'], + 'email_verified_at' => now(), + ]); - if ($imagePath) { - if ($user->image_path) { - Storage::delete($user->image_path); - } - $user->update(['image_path' => $imagePath]); - } - } + $user->notify(new WelcomeNotification); if (! empty($validated['appreciations'])) { foreach ($validated['appreciations'] as $targetUserId) { From 89cdc6cbb1d6f56911dfa6386f97d1f5abe3cfa4 Mon Sep 17 00:00:00 2001 From: TR Tajim Date: Tue, 15 Sep 2026 13:04:26 +0600 Subject: [PATCH 7/7] Update Node.vue --- resources/js/pages/Node.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/pages/Node.vue b/resources/js/pages/Node.vue index af11e92e..7524a8e9 100644 --- a/resources/js/pages/Node.vue +++ b/resources/js/pages/Node.vue @@ -240,7 +240,7 @@ const handleVote = (type: 'up' | 'down') => { class="mt-1.5 text-sm font-semibold text-slate-400 dark:text-gray-500" > {{ parentTitle }} · - {{ totalItemsCount }} Folders Total + {{ totalItemsCount }} Items Total