• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marscoin / martianrepublic / 23804883405

31 Mar 2026 03:14PM UTC coverage: 11.195% (+1.8%) from 9.347%
23804883405

push

github

Martian Congress
refactor: Tier 1 — security fixes, route standardization, code style, docs

S9:  File upload path traversal — assert realpath() within allowed dirs,
     sanitize citizen address in file paths
S11: Citizen registry queries capped with LIMIT 100
S12: External HTTP calls — replaced file_get_contents with Http::timeout()
     in CongressController, Wallet/ApiController, AppHelper
A8:  All routes converted to Laravel 9+ array notation
     [Controller::class, 'method']
D2:  Created DEVELOPMENT.md — complete setup guide for contributors
D4:  Laravel Pint code style enforced across 211 files

Tests updated: public route assertions corrected after route syntax
standardization exposed that old string-notation routes never resolved
properly in tests.

127 tests, 251 assertions, 0 PHPStan errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

302 of 3262 new or added lines in 58 files covered. (9.26%)

139 existing lines in 24 files now uncovered.

620 of 5538 relevant lines covered (11.2%)

1.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/app/Http/Controllers/InstrumentController.php
1
<?php
2

3
namespace App\Http\Controllers;
4

5
use App\Models\Bads\Anomaly;
6
use App\Models\Bads\Attestation;
7
use App\Models\Bads\CalibrationRecord;
8
use App\Models\Bads\Deputy;
9
use App\Models\Bads\Instrument;
10
use App\Models\Bads\OversightCommittee;
11
use App\Models\CivicWallet;
12
use App\Models\Profile;
13
use Illuminate\Http\Request;
14
use Illuminate\Support\Facades\Auth;
15
use Illuminate\Support\Facades\DB;
16
use Illuminate\Support\Facades\View;
17

18
class InstrumentController extends Controller
19
{
20
    /**
21
     * Device category mapping from hex prefix to human-readable category.
22
     */
23
    public const DEVICE_CATEGORIES = [
24
        'atmospheric' => ['prefix' => 0x01, 'label' => 'Atmospheric',  'icon' => 'fa-wind',       'color' => '#00e4ff'],
25
        'power' => ['prefix' => 0x02, 'label' => 'Power',        'icon' => 'fa-bolt',       'color' => '#f59e0b'],
26
        'agricultural' => ['prefix' => 0x03, 'label' => 'Agricultural', 'icon' => 'fa-seedling',   'color' => '#34d399'],
27
        'structural' => ['prefix' => 0x04, 'label' => 'Structural',   'icon' => 'fa-building',   'color' => '#8b5cf6'],
28
        'water' => ['prefix' => 0x05, 'label' => 'Water',        'icon' => 'fa-droplet',    'color' => '#3b82f6'],
29
        'medical' => ['prefix' => 0x06, 'label' => 'Medical',      'icon' => 'fa-heart-pulse', 'color' => '#ef4444'],
30
        'environmental' => ['prefix' => 0x08, 'label' => 'Environmental', 'icon' => 'fa-globe',      'color' => '#f97316'],
31
    ];
32

33
    /**
34
     * Common auth/profile data loader (mirrors CongressController pattern).
35
     */
36
    private function loadUserContext()
×
37
    {
NEW
38
        $ctx = new \stdClass;
×
39
        $ctx->wallet_open = false;
×
40
        $ctx->isCitizen = false;
×
41
        $ctx->isDeputy = false;
×
42
        $ctx->deputyRecord = null;
×
43
        $ctx->public_address = '';
×
44

45
        if (Auth::check()) {
×
46
            $uid = Auth::user()->id;
×
47
            $profile = Profile::where('userid', $uid)->first();
×
48

NEW
49
            if (! $profile) {
×
50
                $ctx->redirect = redirect('/twofa');
×
51

UNCOV
52
                return $ctx;
×
53
            }
54
            if ($profile->openchallenge == 1 || is_null($profile->openchallenge)) {
×
55
                $ctx->redirect = redirect('/twofachallenge');
×
56

UNCOV
57
                return $ctx;
×
58
            }
59

60
            $ctx->wallet_open = $profile->civic_wallet_open;
×
61
            $ctx->isCitizen = $profile->citizen;
×
62

63
            $wallet = CivicWallet::where('user_id', $uid)->first();
×
64
            if ($wallet) {
×
65
                $ctx->public_address = $wallet->public_addr;
×
66
            }
67

68
            $ctx->deputyRecord = Deputy::where('user_id', $uid)->where('status', 'active')->first();
×
69
            $ctx->isDeputy = $ctx->deputyRecord !== null;
×
70
        }
71

72
        return $ctx;
×
73
    }
74

75
    /**
76
     * List all instruments with filtering, grouping, and chain-of-trust stats.
77
     */
78
    public function index()
×
79
    {
80
        $ctx = $this->loadUserContext();
×
NEW
81
        if (isset($ctx->redirect)) {
×
NEW
82
            return $ctx->redirect;
×
83
        }
84

85
        // Load instruments with relationships
86
        $instruments = Instrument::with(['certifiedBy.user', 'certifiedBy.committee'])
×
87
            ->orderBy('created_at', 'desc')
×
88
            ->get();
×
89

90
        // Category counts
91
        $categoryCounts = Instrument::select('device_category', DB::raw('COUNT(*) as cnt'))
×
92
            ->groupBy('device_category')
×
93
            ->pluck('cnt', 'device_category')
×
94
            ->toArray();
×
95

96
        // Chain of trust stats
97
        $stats = [
×
NEW
98
            'committees' => OversightCommittee::where('status', 'active')->count(),
×
NEW
99
            'deputies' => Deputy::where('status', 'active')->count(),
×
NEW
100
            'instruments' => Instrument::count(),
×
101
            'attestations' => Attestation::count(),
×
102
        ];
×
103

104
        // Committees for sidebar
105
        $committees = OversightCommittee::withCount(['deputies', 'instruments'])
×
106
            ->where('status', 'active')
×
107
            ->get();
×
108

109
        $view = View::make('inventory.instruments');
×
110
        $view->instruments = $instruments;
×
111
        $view->categoryCounts = $categoryCounts;
×
112
        $view->stats = $stats;
×
113
        $view->committees = $committees;
×
114
        $view->categories = self::DEVICE_CATEGORIES;
×
115
        $view->wallet_open = $ctx->wallet_open;
×
116
        $view->isCitizen = $ctx->isCitizen;
×
117
        $view->isDeputy = $ctx->isDeputy;
×
118

119
        return $view;
×
120
    }
121

122
    /**
123
     * Show single instrument detail with full chain of trust.
124
     */
125
    public function show($id)
×
126
    {
127
        $ctx = $this->loadUserContext();
×
NEW
128
        if (isset($ctx->redirect)) {
×
NEW
129
            return $ctx->redirect;
×
130
        }
131

132
        $instrument = Instrument::with([
×
133
            'certifiedBy.user',
×
134
            'certifiedBy.committee.proposal',
×
135
        ])->findOrFail($id);
×
136

137
        $attestations = Attestation::where('instrument_id', $id)
×
138
            ->orderBy('created_at', 'desc')
×
139
            ->limit(20)
×
140
            ->get();
×
141

142
        $anomalies = Anomaly::where('instrument_id', $id)
×
143
            ->orderBy('created_at', 'desc')
×
144
            ->limit(20)
×
145
            ->get();
×
146

147
        $calibrations = CalibrationRecord::where('instrument_id', $id)
×
148
            ->with('calibrator.user')
×
149
            ->orderBy('calibrated_at', 'desc')
×
150
            ->get();
×
151

152
        // Build chain of trust path
153
        $chain = [];
×
154
        if ($instrument->certifiedBy && $instrument->certifiedBy->committee) {
×
155
            $committee = $instrument->certifiedBy->committee;
×
156
            $chain['proposal'] = $committee->proposal;
×
157
            $chain['committee'] = $committee;
×
158
            $chain['deputy'] = $instrument->certifiedBy;
×
159
        }
160
        $chain['instrument'] = $instrument;
×
161

162
        $view = View::make('inventory.instrument-detail');
×
163
        $view->instrument = $instrument;
×
164
        $view->attestations = $attestations;
×
165
        $view->anomalies = $anomalies;
×
166
        $view->calibrations = $calibrations;
×
167
        $view->chain = $chain;
×
168
        $view->categories = self::DEVICE_CATEGORIES;
×
169
        $view->wallet_open = $ctx->wallet_open;
×
170
        $view->isCitizen = $ctx->isCitizen;
×
171
        $view->isDeputy = $ctx->isDeputy;
×
172

173
        return $view;
×
174
    }
175

176
    /**
177
     * Show instrument registration form (deputy-only).
178
     */
179
    public function create()
×
180
    {
181
        $ctx = $this->loadUserContext();
×
NEW
182
        if (isset($ctx->redirect)) {
×
NEW
183
            return $ctx->redirect;
×
184
        }
185

NEW
186
        if (! $ctx->isDeputy) {
×
187
            return redirect()->route('instruments.index')
×
188
                ->with('error', 'Only active deputies may certify new instruments.');
×
189
        }
190

191
        // Committees the current user is deputy of
192
        $myCommittees = OversightCommittee::whereHas('deputies', function ($q) {
×
193
            $q->where('user_id', Auth::id())->where('status', 'active');
×
194
        })->get();
×
195

196
        $view = View::make('inventory.instrument-create');
×
197
        $view->deviceTypes = Instrument::DEVICE_TYPES;
×
198
        $view->categories = self::DEVICE_CATEGORIES;
×
199
        $view->myCommittees = $myCommittees;
×
200
        $view->wallet_open = $ctx->wallet_open;
×
201
        $view->isCitizen = $ctx->isCitizen;
×
202
        $view->isDeputy = $ctx->isDeputy;
×
203

204
        return $view;
×
205
    }
206

207
    /**
208
     * Store a new instrument registration.
209
     */
210
    public function store(Request $request)
×
211
    {
212
        $ctx = $this->loadUserContext();
×
NEW
213
        if (isset($ctx->redirect)) {
×
NEW
214
            return $ctx->redirect;
×
215
        }
216

NEW
217
        if (! $ctx->isDeputy) {
×
218
            return redirect()->route('instruments.index')
×
219
                ->with('error', 'Only active deputies may certify new instruments.');
×
220
        }
221

222
        $request->validate([
×
NEW
223
            'device_type' => 'required|integer',
×
NEW
224
            'serial' => 'required|string|max:255',
×
NEW
225
            'make' => 'nullable|string|max:255',
×
NEW
226
            'model' => 'nullable|string|max:255',
×
NEW
227
            'location' => 'nullable|string|max:255',
×
228
            'firmware_version' => 'nullable|string|max:50',
×
NEW
229
            'mqtt_namespace' => 'nullable|string|max:255',
×
230
        ]);
×
231

232
        $deviceType = (int) $request->input('device_type');
×
233
        $deviceTypeName = Instrument::DEVICE_TYPES[$deviceType] ?? 'Unknown';
×
234

235
        // Derive category from device type hex prefix
236
        $prefix = $deviceType >> 8;
×
237
        $deviceCategory = 'unknown';
×
238
        foreach (self::DEVICE_CATEGORIES as $key => $cat) {
×
239
            if ($cat['prefix'] === $prefix) {
×
240
                $deviceCategory = $key;
×
241
                break;
×
242
            }
243
        }
244

245
        // Generate a Marscoin address for the device
NEW
246
        $address = 'M'.substr(hash('sha256', uniqid('bads_', true).$request->input('serial')), 0, 33);
×
247

248
        $instrument = Instrument::create([
×
NEW
249
            'address' => $address,
×
NEW
250
            'device_type' => $deviceType,
×
NEW
251
            'device_type_name' => $deviceTypeName,
×
NEW
252
            'device_category' => $deviceCategory,
×
NEW
253
            'make' => $request->input('make'),
×
NEW
254
            'model' => $request->input('model'),
×
NEW
255
            'serial' => $request->input('serial'),
×
NEW
256
            'location' => $request->input('location'),
×
NEW
257
            'firmware_version' => $request->input('firmware_version'),
×
NEW
258
            'mqtt_namespace' => $request->input('mqtt_namespace'),
×
NEW
259
            'certified_by_deputy_id' => $ctx->deputyRecord->id,
×
NEW
260
            'status' => 'active',
×
UNCOV
261
        ]);
×
262

263
        return redirect()->route('instruments.show', $instrument->id)
×
264
            ->with('success', 'Instrument certified and registered successfully.');
×
265
    }
266

267
    /**
268
     * List all oversight committees with their deputies and instrument counts.
269
     */
270
    public function committees()
×
271
    {
272
        $ctx = $this->loadUserContext();
×
NEW
273
        if (isset($ctx->redirect)) {
×
NEW
274
            return $ctx->redirect;
×
275
        }
276

277
        $committees = OversightCommittee::with(['deputies.user', 'proposal'])
×
278
            ->withCount(['deputies', 'instruments'])
×
279
            ->orderBy('name')
×
280
            ->get();
×
281

282
        $view = View::make('inventory.committees');
×
283
        $view->committees = $committees;
×
284
        $view->wallet_open = $ctx->wallet_open;
×
285
        $view->isCitizen = $ctx->isCitizen;
×
286
        $view->isDeputy = $ctx->isDeputy;
×
287

288
        return $view;
×
289
    }
290

291
    /**
292
     * JSON endpoint: full chain of trust for an instrument.
293
     */
294
    public function chainOfTrust($instrumentId)
×
295
    {
296
        $instrument = Instrument::with([
×
297
            'certifiedBy.user',
×
298
            'certifiedBy.committee.proposal',
×
NEW
299
            'attestations' => function ($q) {
×
NEW
300
                $q->orderBy('created_at', 'desc')->limit(10);
×
NEW
301
            },
×
UNCOV
302
        ])->findOrFail($instrumentId);
×
303

304
        $chain = [
×
305
            'instrument' => [
×
NEW
306
                'id' => $instrument->id,
×
NEW
307
                'address' => $instrument->address,
×
NEW
308
                'name' => $instrument->device_type_name,
×
NEW
309
                'serial' => $instrument->serial,
×
NEW
310
                'status' => $instrument->status,
×
NEW
311
                'cert_txid' => $instrument->cert_txid,
×
312
            ],
×
313
        ];
×
314

315
        if ($instrument->certifiedBy) {
×
316
            $deputy = $instrument->certifiedBy;
×
317
            $chain['deputy'] = [
×
NEW
318
                'id' => $deputy->id,
×
319
                'civic_address' => $deputy->civic_address,
×
NEW
320
                'user_name' => $deputy->user ? $deputy->user->fullname : 'Unknown',
×
NEW
321
                'role_tag' => $deputy->role_tag,
×
322
                'appointment_txid' => $deputy->appointment_txid,
×
323
            ];
×
324

325
            if ($deputy->committee) {
×
326
                $committee = $deputy->committee;
×
327
                $chain['committee'] = [
×
NEW
328
                    'id' => $committee->id,
×
NEW
329
                    'name' => $committee->name,
×
NEW
330
                    'slug' => $committee->slug,
×
NEW
331
                    'proposal_txid' => $committee->proposal_txid,
×
UNCOV
332
                ];
×
333

334
                if ($committee->proposal) {
×
335
                    $chain['proposal'] = [
×
NEW
336
                        'id' => $committee->proposal->id,
×
337
                        'title' => $committee->proposal->title,
×
NEW
338
                        'status' => $committee->proposal->status,
×
NEW
339
                        'txid' => $committee->proposal->ipfs_hash ?? null,
×
UNCOV
340
                    ];
×
341
                }
342
            }
343
        }
344

345
        $chain['attestations'] = $instrument->attestations->map(function ($a) {
×
346
            return [
×
NEW
347
                'id' => $a->id,
×
NEW
348
                'txid' => $a->txid,
×
349
                'merkle_root' => $a->merkle_root,
×
NEW
350
                'readings' => $a->reading_count,
×
NEW
351
                'verified' => $a->verified,
×
352
                'batch_start' => $a->batch_start ? $a->batch_start->toIso8601String() : null,
×
NEW
353
                'batch_end' => $a->batch_end ? $a->batch_end->toIso8601String() : null,
×
354
            ];
×
355
        });
×
356

357
        return response()->json($chain);
×
358
    }
359
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc