Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
RolePlaySessionStartRequest
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
2 / 2
9
100.00% covered (success)
100.00%
1 / 1
 authorize
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
8
 rules
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Requests\v2\RolePlay;
4
5use App\Http\Models\RolePlayConversations;
6use App\Http\Models\RolePlayProjects;
7use App\Http\Models\UserAddOns;
8use Illuminate\Foundation\Http\FormRequest;
9use MongoDB\BSON\UTCDateTime;
10
11class RolePlaySessionStartRequest extends FormRequest
12{
13    /**
14     * Determine if the user is authorized to make this request.
15     *
16     * Authorization requires both:
17     *  1. Ownership — the requested project_id must belong to the caller.
18     *     Without this, any authenticated user could start a session against
19     *     another user's persona and read its ICP / prompt data (IDOR).
20     *  2. Quota — the caller must have an active RolePlay addon and enough
21     *     remaining monthly session / time credits.
22     */
23    public function authorize(): bool
24    {
25        $user = $this->user();
26
27        // Ownership check — defends against IDOR where a caller passes another
28        // user's project_id and receives its ICP/prompt data in the response.
29        // We let rules() handle the missing/non-existent project_id cases via
30        // the `exists:` validator (so those return 422, not 403).
31        $projectId = $this->input('project_id');
32        if ($projectId) {
33            $project = RolePlayProjects::find($projectId);
34            if ($project && (string) $project->user_id !== (string) $user->id) {
35                return false;
36            }
37        }
38
39        $rolePlayAddOn = UserAddOns::where('user_id', $user->id)
40            ->where('product', 'RolePlay')
41            ->where('status', 'active')
42            ->orderBy('created_at', 'desc')
43            ->first();
44
45        if (! $rolePlayAddOn) {
46            return false;
47        }
48
49        $rolePlayAddOn->load('addOn');
50
51        $monthly_total_time_credits = $rolePlayAddOn->addOn->features['monthly_total_time_credits'] ?? 0;
52        $monthly_roleplay_credits = $rolePlayAddOn->addOn->features['monthly_roleplay_credits'] ?? 0;
53
54        $usedRolePlayCredits = RolePlayConversations::where('user_id', $user->id)
55            ->where('created_at', '>=', new UTCDateTime(now()->startOfMonth()->getTimestampMs()))
56            ->count() ?? 0;
57
58        $usedRolePlayTimeCredits = RolePlayConversations::where('user_id', $user->id)
59            ->where('created_at', '>=', new UTCDateTime(now()->startOfMonth()->getTimestampMs()))
60            ->sum('duration') ?? 0;
61
62        return ($monthly_roleplay_credits === -1 || $usedRolePlayCredits < $monthly_roleplay_credits)
63            && ($monthly_total_time_credits === -1 || $usedRolePlayTimeCredits < $monthly_total_time_credits);
64    }
65
66    /**
67     * Get the validation rules that apply to the request.
68     *
69     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
70     */
71    public function rules(): array
72    {
73        return [
74            'project_id' => 'required|exists:role_play_projects,id',
75            'icp' => 'required|json',
76            'agent' => 'nullable|json',
77            'prompt' => 'nullable|string',
78        ];
79    }
80}