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

marscoin / martianrepublic / 23751722506

30 Mar 2026 03:03PM UTC coverage: 8.391% (-0.01%) from 8.401%
23751722506

push

github

Martian Congress
refactor: Sprint 2 — architecture cleanup (A3, A4, A7, A9)

A3: Added $fillable to 7 models (Proposals, Vote, Threads, HDWallet,
    IPFSRoot, Publication, Voucher) — closes mass assignment risk

A4: Created config/blockchain.php centralizing all hardcoded URLs
    (RPC, Pebas, IPFS, Explorer, Ballot, Price). Updated 7 files
    to use config() instead of hardcoded strings.

A7: Removed dead code:
    - Deleted ApiController.php.bak, AppHelper.php.bak
    - Deleted unused models: Pkimport.php, Recovery.php
    - Deleted resources/views/wallet/testing.html
    - Removed 8 commented-out routes from web.php and api.php

A9: Added performance indexes migration:
    - proposals.status (phase sync, tab filtering)
    - ballots.userid, ballots.proposalid, ballots(userid,status)
    - votes.proposal_id (vote tallying)
    - forum_posts.author_id, forum_threads.author_id (joins)

A1 (controller split) deferred to dedicated session — too much
in-page JS coupling to batch safely.

79 tests passing, 0 PHPStan errors.

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

2 of 48 new or added lines in 7 files covered. (4.17%)

2 existing lines in 2 files now uncovered.

445 of 5303 relevant lines covered (8.39%)

1.06 hits per line

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

4.89
/app/Http/Controllers/ApiController.php
1
<?php
2
namespace App\Http\Controllers;
3

4
use App\Http\Controllers\Controller;
5
use Illuminate\Http\Request;
6
use Illuminate\Http\Response;
7
use Illuminate\Support\Facades\Auth;
8
use App\Includes\jsonRPCClient;
9
use App\Includes\AppHelper;
10
use App\Models\Feed;
11
use App\Models\Profile;
12
use App\Models\User;
13
use App\Models\Proposals;
14
use App\Models\Threads;
15
use App\Models\Posts;
16
use App\Models\Citizen;
17
use App\Models\HDWallet;
18
use App\Models\CivicWallet;
19
use App\Models\MSession;
20
use Illuminate\Support\Facades\DB;
21
use Illuminate\Support\Str;
22
use Carbon\Carbon;
23
use Illuminate\Support\Facades\Cache;
24
use Illuminate\Support\Facades\Hash;
25
use Illuminate\Support\Facades\Log;
26
use BitcoinPHP\BitcoinECDSA\BitcoinECDSA;
27
use App\Includes\MarscoinECDSA;
28
use App\Livewire\CitizenStats;
29
use Illuminate\Database\Eloquent\ModelNotFoundException;
30
use App\Includes\IPFSRoot;
31

32
class ApiController extends Controller
33
{
34
    public function allPublic()
1✔
35
    {
36
        $perPage = 25;
1✔
37
        $cacheKey = 'all_public_cache'; // Define a unique cache key for this query
1✔
38
        $excludedUserIds = [6462, 7601]; // ID of the user you want to exclude, for example, a test account
1✔
39

40
        // Attempt to get cached data. If not available, the closure will be run to fetch and cache the data.
41
        $feeds = Cache::remember($cacheKey, 60, function () use ($perPage, $excludedUserIds) {
1✔
42
            $feeds = Feed::with(['user' => function ($query) use ($excludedUserIds) {
1✔
43
                $query->select('id', 'fullname', 'created_at')
×
44
                      ->where('id', '!=', $excludedUserIds); // Exclude the specific user by ID
×
45
            }, 'user.profile' => function ($query) {
1✔
46
                $query->select('userid', 'general_public', 'endorse_cnt', 'citizen', 'has_application');
×
47
            }, 'user.citizen' => function ($query) {
1✔
48
                $query->select('userid', 'avatar_link', 'liveness_link')
×
49
                      ->whereNotNull('avatar_link');
×
50
            }])
1✔
51
            ->joinSub(
1✔
52
                Feed::selectRaw('max(id) as latest_id, userid')
1✔
53
                    ->where('tag', 'GP') // Ensure the tag is 'GP'
1✔
54
                    ->groupBy('userid'),
1✔
55
                'latest_feeds',
1✔
56
                function($join) {
1✔
57
                    $join->on('feed.id', '=', 'latest_feeds.latest_id');
1✔
58
                }
1✔
59
            )
1✔
60
            ->whereHas('user.profile', function ($query) {
1✔
61
                $query->where('tag', 'GP');
1✔
62
            })
1✔
63
            ->whereHas('user', function ($query) use ($excludedUserIds) {
1✔
64
                $query->whereNotIn('id', $excludedUserIds);
1✔
65
            })
1✔
66
            ->orderByDesc('id')
1✔
67
            ->take($perPage)
1✔
68
            ->distinct()
1✔
69
            ->get();
1✔
70

71
            return $feeds; // Directly return the fetched feeds
1✔
72
        });
1✔
73

74
        return response()->json($feeds);
1✔
75
    }
76

77

78

79

80
    public function allCitizen()
×
81
    {
82
        $perPage = 25;
×
83
        $cacheKey = 'all_citizens_cache';
×
84
        $excludedUserId = '';
×
85

86
        $feeds = Cache::remember($cacheKey, 60, function () use ($perPage, $excludedUserId) {
×
87
            return Feed::with([
×
88
                'user' => function ($query) use ($excludedUserId) {
×
89
                    $query->where('id', '!=', $excludedUserId)
×
90
                          ->select('id', 'fullname', 'created_at');
×
91
                },
×
92
                'user.profile' => function ($query) {
×
93
                    $query->select('userid', 'general_public', 'endorse_cnt', 'citizen', 'has_application');
×
94
                },
×
95
                'user.citizen' => function ($query) {
×
96
                    $query->select('userid', 'avatar_link', 'liveness_link')
×
97
                          ->whereNotNull('avatar_link'); // Ensure that avatar_link is not NULL
×
98
                }
×
99
            ])
×
100
            ->whereHas('user', function ($query) use ($excludedUserId) {  // Ensure user exists and is not excluded
×
101
                $query->where('id', '!=', $excludedUserId);
×
102
            })
×
103
            ->whereHas('user.citizen', function ($query) {  // Ensure user has valid citizen data
×
104
                $query->whereNotNull('avatar_link');
×
105
            })
×
106
            ->whereHas('user.profile', function ($query) {  // Additional filters for the profile
×
107
                $query->where('tag', 'CT');
×
108
            })
×
109
            ->select('id', 'address', 'userid', 'tag', 'message', 'embedded_link', 'txid', 'blockid', 'mined', 'updated_at', 'created_at')  // Only select columns from the feed table
×
110
            ->orderByDesc('id')
×
111
            ->take($perPage)
×
112
            ->get();
×
113
        });
×
114

115
        return response()->json($feeds);
×
116
    }
117

118

119

120

121

122
    // public function allApplicants()
123
    // {
124
    //     $perPage = 25;
125

126
    //     $applicants = User::whereHas('profile', function ($query) {
127
    //         $query->where('has_application', 1);
128
    //     })
129
    //     ->with(['profile', 'hdWallet' => function($query) {
130
    //         // If you only need the public_addr from HdWallet, select it specifically
131
    //         $query->select('user_id', 'public_addr');
132
    //     }])
133
    //     ->orderByDesc('id')
134
    //     ->paginate($perPage, ['id', 'fullname']);
135
    //     // Customize the result to match the raw query structure, if needed
136
    //     $customResult = $applicants->getCollection()->transform(function ($user) {
137
    //         return [
138
    //             'userid' => $user->id,
139
    //             'fullname' => $user->fullname,
140
    //             'address' => $user->hdWallet ? $user->hdWallet->public_addr : null,
141
    //         ];
142
    //     });
143

144
    //     return response()->json([
145
    //         'current_page' => $applicants->currentPage(),
146
    //         'data' => $customResult,
147
    //         'total' => $applicants->total(),
148
    //         'per_page' => $applicants->perPage(),
149
    //         'last_page' => $applicants->lastPage(),
150
    //     ]);
151
    // }
152

153
    public function allApplicants()
×
154
    {
155
        $perPage = 25;
×
156

157
        $applicants = User::whereHas('profile', function ($query) {
×
158
            $query->where('has_application', 1);
×
159
        })
×
160
        ->where('id', '!=', 6462) // Exclude user with id 6462
×
161
        ->with(['profile', 'hdWallet' => function($query) {
×
162
            $query->select('user_id', 'public_addr');
×
163
        }, 'citizen']) // Add citizen relationship
×
164
        ->orderByDesc('id')
×
165
        ->paginate($perPage, ['id', 'fullname']);
×
166

167
        $customResult = $applicants->getCollection()->transform(function ($user) {
×
168
            return [
×
169
                'userid' => $user->id,
×
170
                'fullname' => $user->fullname,
×
171
                'address' => $user->hdWallet ? $user->hdWallet->public_addr : null,
×
172
                'citizen' => $user->citizen, // Include citizen data
×
173
            ];
×
174
        });
×
175

176
        return response()->json([
×
177
            'current_page' => $applicants->currentPage(),
×
178
            'data' => $customResult,
×
179
            'total' => $applicants->total(),
×
180
            'per_page' => $applicants->perPage(),
×
181
            'last_page' => $applicants->lastPage(),
×
182
        ]);
×
183
    }
184

185

186
    public function allFeed(Request $request)
×
187
    {
188
        $perPage = 25;
×
189
        $page = $request->input('page', 1);
×
190
        $excludedUserIds = [6462, 7601];
×
191
        $cacheKey = 'all_feed_cache_' . $page;
×
192

193
        $feeds = Cache::remember($cacheKey, 60, function () use ($perPage, $excludedUserIds) {
×
194
            return Feed::with(['user' => function ($query) use ($excludedUserIds) {
×
195
                $query->select('id', 'fullname', 'created_at')
×
196
                    ->whereNotIn('id', $excludedUserIds);
×
197
            }, 'user.profile' => function ($query) {
×
198
                $query->select('userid', 'general_public', 'endorse_cnt', 'citizen', 'has_application');
×
199
            }, 'user.citizen' => function ($query) {
×
200
                $query->select('userid', 'avatar_link', 'liveness_link')
×
201
                    ->whereNotNull('avatar_link');
×
202
            }])
×
203
            ->whereHas('user', function ($query) use ($excludedUserIds) {
×
204
                $query->whereNotIn('id', $excludedUserIds);
×
205
            })
×
206
            ->whereNotNull('mined')
×
207
            ->select(
×
208
                'id',
×
209
                'address',
×
210
                'userid',
×
211
                'tag',
×
212
                'message',
×
213
                'embedded_link',
×
214
                'txid',
×
215
                'blockid',
×
216
                'mined',
×
217
                'created_at',
×
218
                'updated_at'
×
219
            )
×
220
            ->orderByDesc('mined')
×
221
            ->orderByDesc('id')
×
222
            ->paginate($perPage);
×
223
        });
×
224

225
        return $this->paginatedResponse($feeds);
×
226
    }
227

228
    // Helper method to format paginated responses consistently
229
    private function paginatedResponse($feeds)
×
230
    {
231
        return response()->json([
×
232
            'current_page' => $feeds->currentPage(),
×
233
            'data' => $feeds->items(),
×
234
            'first_page_url' => $feeds->url(1),
×
235
            'from' => $feeds->firstItem(),
×
236
            'last_page' => $feeds->lastPage(),
×
237
            'last_page_url' => $feeds->url($feeds->lastPage()),
×
238
            'next_page_url' => $feeds->nextPageUrl(),
×
239
            'path' => $feeds->path(),
×
240
            'per_page' => $feeds->perPage(),
×
241
            'prev_page_url' => $feeds->previousPageUrl(),
×
242
            'to' => $feeds->lastItem(),
×
243
            'total' => $feeds->total(),
×
244
        ]);
×
245
    }
246

247

248
    public function showCitizen(Request $request, $address)
×
249
    {
250
        // Look up the Martian user by public address
251
        $martianWallet = CivicWallet::where('public_addr', $address)->first();
×
252

253
        if (!$martianWallet) {
×
254
            return response()->json(['message' => 'Martian not found.'], 404);
×
255
        }
256

257
        // Attempt to fetch the user's profile and additional data
258
        $citizen = Citizen::where('public_address', $address)->first();
×
259
        $profile = Profile::where('userid', $martianWallet->user_id)->first();
×
260
        $feedItems = Feed::where('userid', $martianWallet->user_id)->whereNotNull('mined')->whereNotIn('tag', ['GP','CT'])->orderBy('created_at', 'desc')->get();
×
261

262
        // Fetch the latest 3 activities
263
        $activity = DB::table('feed')
×
264
            ->join('users', 'feed.userid', '=', 'users.id')
×
265
            ->join('profile', 'feed.userid', '=', 'profile.userid')
×
266
            ->select('profile.userid', 'users.fullname', 'feed.tag', 'feed.mined')
×
267
            ->where('feed.userid', $martianWallet->user_id)
×
268
            ->orderBy('feed.id', 'desc')
×
269
            ->get();
×
270

271
        // Construct response
272
        $response = [
×
273
            'citizen' => $citizen,
×
274
            'profile' => [
×
275
                'general_public' => $profile ? $profile->general_public : null,
×
276
                'isCitizen' => $profile ? $profile->citizen : null,
×
277
            ],
×
278
            'feedItems' => $feedItems,
×
279
            'activity' => $activity,
×
280
        ];
×
281

282
        return response()->json($response);
×
283
    }
284

285

286

287
    public function token(Request $request)
×
288
    {
289
        $publicAddress = $request->input('a');
×
290
        $msg = $request->input('m');
×
291
        $sig = $request->input('s');
×
292
        $timestamp = $request->input('t');
×
293

294
        Log::debug("Address: " . $publicAddress);
×
295
        Log::debug("msg: " . $msg);
×
296
        Log::debug("sig: " . $sig);
×
297
        Log::debug("timestamp: " . $timestamp);
×
298

299
        if (empty($msg)){
×
300
            $data = $request->json()->all();
×
301

302
            if (isset($data['data'])) {
×
303
                $msg = $data['data']['m'] ?? null;
×
304
                $sig = $data['data']['s'] ?? null;
×
305
                $publicAddress = $data['data']['a'] ?? null;
×
306
                $timestamp = $data['data']['t'] ?? '0';
×
307
            }
308
        }
309
        // Validate timestamp
310
        if (Carbon::now()->diffInSeconds(Carbon::createFromTimestamp($timestamp)) > 600) { // 5 minutes tolerance
×
311
            return response()->json(['error' => 'Request timestamp is too old.'], 401);
×
312
        }
313

314
        $bitcoinECDSA = new BitcoinECDSA();
×
315
        $marscoinECDSA = new MarscoinECDSA();
×
316
        if ($marscoinECDSA->checkSignatureForMessage($publicAddress, $sig, $msg))
×
317
        {
318
            $wallet = CivicWallet::where('public_addr', $publicAddress)->first();
×
319

320
            if ($wallet) {
×
321
                // Wallet found, get the associated user
322
                $user = $wallet->user;
×
323
            } else {
324
                // Wallet not found, create a new user without a wallet
325
                $user = User::where('email', $publicAddress . '@martianrepublic.org')->first();
×
326
                if (!$user) {
×
327
                    $user = User::create([
×
328
                        'fullname' => 'UserWithoutWallet', // Or any other default or generated name
×
329
                        'email' => $publicAddress . '@martianrepublic.org',
×
330
                        'password' => Hash::make(Str::random(10)), // Random password
×
331
                    ]);
×
332
                    Log::debug("..created user");
×
333
                }
334
                $profile = Profile::where('userid', $user->id)->first();
×
335
                if (!$profile) {
×
336
                    $profile = Profile::create([
×
337
                        'userid' => $user->id,
×
338
                        'wallet_open' => 0,
×
339
                        'civic_wallet_open' => 0,
×
340
                        'has_application' => 0,
×
341
                    ]);
×
342
                    Log::debug("..created profile");
×
343
                }
344
                //create a citcache entry
345
                $citcache = Citizen::where('userid', $user->id)->first();
×
346
                if (!$citcache) {
×
347
                    $citizen = Citizen::create([
×
348
                        'userid' => $user->id,
×
349
                        'public_address' => $publicAddress,
×
350
                        'created_at' => now(),
×
351
                        'updated_at' => now(),
×
352
                    ]);
×
353
                    Log::debug("..created citizen cache");
×
354
                }
355
                // register civic wallet for the new user
356
                $wallet = CivicWallet::create([
×
357
                    'user_id' => $user->id,
×
358
                    'wallet_type' => 'MARS',
×
359
                    'backup' => 0,
×
360
                    'encrypted_seed' => '',
×
361
                    'public_addr' => $publicAddress,
×
362
                    'created_at' => now(),
×
363
                    'opened_at' => now()
×
364
                ]);
×
365
                Log::debug("..created civic wallet");
×
366
            }
367
            if($user->status == 'inactive')
×
368
                return response()->json(['token' => 'inactive']);
×
369

370
            $token = $user->createToken('authToken')->plainTextToken;
×
371

372
            return response()->json(['token' => $token]);
×
373
        }
374
        else {
375
            return response()->json(['message' => 'couldnt verify message'], 406);
×
376
        }
377
    }
378

379
    public function test()
×
380
    {
381

382
        $messageM="https://martianrepublic.org/api/token?a=MCHe2XTUEegyqsYc5ePe2dQiPtixLfhppR&t=1715354045";
×
383
        $addressM="MCHe2XTUEegyqsYc5ePe2dQiPtixLfhppR";
×
384
        $signatureM="H+Qrav6eWNrB0P5DiRaGRRR/RVtD8qd5dKlWA3FOfeiFc4h04769HtfMbsmxrrNPk0MeTJmwPCR9xg67f1NatOA=";
×
385

386
        $bitcoinECDSA = new BitcoinECDSA();
×
387
        $bitcoinECDSA->generateRandomPrivateKey(); //generate new random private key
×
388
        $address = $bitcoinECDSA->getAddress();
×
389
        $message = "Test message";
×
390
        $signedMessage = $bitcoinECDSA->signMessage($message, true);
×
391
        echo "message: " . PHP_EOL . "<br>";
×
392
        echo $message . PHP_EOL . "<br>";
×
393
        echo "signed message: " . PHP_EOL . "<br>";
×
394
        echo $signedMessage . PHP_EOL . "<br>";
×
395
        //$signedMessage = "random";
396

397
        //loading Bitcoin crypto library
398
        if ($bitcoinECDSA->checkSignatureForMessage($address, $signedMessage, $message))       //verifying signature
×
399
        {
400
            Log::debug("True");
×
401
            echo "True<br>";
×
402
        }else{
403
            Log::debug("False");
×
404
            echo "False<br>";
×
405
        }
406

407
        echo "mars test";
×
408
        $marscoinECDSA = new MarscoinECDSA();
×
409
        echo "Address; " . $addressM;
×
410
        echo "Message:" . $messageM;
×
411
        echo "sig:" . $signatureM;
×
412
         //loading Bitcoin crypto library
413
         if ($marscoinECDSA->checkSignatureForMessage($addressM, $signatureM, $messageM))       //verifying signature
×
414
         {
415
             Log::debug("True");
×
416
             echo "True<br>";
×
417
         }else{
418
             Log::debug("False");
×
419
             echo "False<br>";
×
420
         }
421
    }
422

423

424

425
    //token access
426
    public function scitizen(Request $request)
×
427
        {
428

429
        $uid = Auth::user()->id;
×
430
        $citcache = Citizen::where('userid', '=', $uid)->first();
×
431
        if(is_null($citcache)) $citcache = new Citizen;
×
432
        $citcache->userid = $uid;
×
433

434
        $firstname = $request->input('firstname');
×
435
        $lastname = $request->input('lastname');
×
436
        $shortbio = $request->input('shortbio');
×
437
        $displayname = $request->input('displayname');
×
438

439
        if(isset($firstname) && isset($lastname))
×
440
        {
441
            $fullname = $firstname . " " . $lastname;
×
442

443
            $citcache->firstname = $firstname;
×
444
            $citcache->lastname = $lastname;
×
445
            $user = User::where('id', '=', $uid)->first();
×
446
            $user->fullname = $fullname;
×
447
            $user->save();
×
448
            $profile = Profile::where('userid', '=', $uid)->first();
×
449
            $profile->has_application = 1;
×
450
            $profile->save();
×
451
        }
452
        if(isset($shortbio))
×
453
        {
454
            $citcache->shortbio = $shortbio;
×
455
            $profile = Profile::where('userid', '=', $uid)->first();
×
456
            $profile->has_application = 1;
×
457
            $profile->save();
×
458
        }
459
        if(isset($displayname))
×
460
        {
461
            $citcache->displayname = $displayname;
×
462
            $profile = Profile::where('userid', '=', $uid)->first();
×
463
            $profile->has_application = 1;
×
464
            $profile->save();
×
465
        }
466

467
        $citcache->save();
×
468
        // Fetch profile data
469
        $profile = Profile::where('userid', '=', $uid)->first();
×
470

471
        // Merge citizen and profile data
472
        $response = [
×
473
            'citizen' => $citcache,
×
474
            'profile' => [
×
475
                'citizen' => $profile->citizen ?? null,
×
476
                'endorse_cnt' => $profile->endorse_cnt ?? null,
×
477
                'general_public' => $profile->general_public ?? null,
×
478
                'has_application' => $profile->has_application ?? null,
×
479
            ]
×
480
        ];
×
481

482
        // Return the merged data as a JSON response
483
        return response()->json($response);
×
484
        }
485

486

487
    public function pinpic(Request $request)
×
488
    {
489
                $uid = Auth::user()->id;
×
490
        $hash = "";
×
491
        $dataPic = $request->input('picture');
×
492
        $type = $request->input('type');
×
493
        $public_address = $request->input('address');
×
494

495
        // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
496
        $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
497
        if ($safeAddress === null) {
×
498
            return response()->json(['error' => 'Invalid address format.'], 400);
×
499
        }
500

501
        // --- SECURITY: Validate the base64 image data (extension, MIME, size, PHP code) ---
502
        $validation = AppHelper::validateBase64Image($dataPic);
×
503
        if (!$validation['valid']) {
×
504
            Log::warning('pinpic upload rejected: ' . $validation['error'] . ' (user: ' . $uid . ')');
×
505
            return response()->json(['error' => $validation['error']], 422);
×
506
        }
507

508
        $safeExtension = $validation['extension'];
×
509
        $decodedData = $validation['data'];
×
510

511
        $file_path = "./assets/citizen/" . $safeAddress . "/";
×
512
        if (!file_exists($file_path)) {
×
513
            mkdir($file_path, 0755, true);
×
514
        }
515

516
        // --- SECURITY: Write .htaccess to prevent PHP execution in upload dir ---
517
        AppHelper::writeUploadHtaccess($file_path);
×
518

519
        // Use validated extension, not user-supplied one
520
        $file_path = "./assets/citizen/" . $safeAddress . "/profile_pic." . $safeExtension;
×
521

522
        file_put_contents($file_path, $decodedData);
×
NEW
523
        $hash = AppHelper::upload($file_path, config('blockchain.ipfs.api_url') . "/api/v0/add?pin=true");
×
524

525
        $citcache = Citizen::where('userid', '=', $uid)->first();
×
526
        if(is_null($citcache)) $citcache = new Citizen;
×
527
        $citcache->userid = $uid;
×
NEW
528
        $citcache->avatar_link = config('blockchain.ipfs.gateway_url').$hash;
×
529
        $citcache->save();
×
530

531
        return (new Response(json_encode(array("Hash" => $hash)), 200))
×
532
            ->header('Content-Type', "application/json;");
×
533

534
        }
535

536

537

538
        public function pinvideo(Request $request){
×
539

540
        Log::debug('pinvid');
×
541
        $uid = Auth::user()->id;
×
542
        $hash = "";
×
543
        $dataPic = $request->input('file');
×
544
        $type = $request->input('type');
×
545
        $public_address = $request->input('address');
×
546

547
        // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
548
        $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
549
        if ($safeAddress === null) {
×
550
            return response()->json(['error' => 'Invalid address format.'], 400);
×
551
        }
552

553
        Log::debug('public: ' . $safeAddress);
×
554
        if ($request->hasFile('file'))
×
555
        {
556
            // --- SECURITY: Validate the uploaded file (extension, MIME, size, PHP code) ---
557
            $uploadedFile = $request->file('file');
×
558
            $validation = AppHelper::validateUploadedFile($uploadedFile, [
×
559
                'webm' => ['video/webm', 'audio/webm'],
×
560
            ]);
×
561
            if (!$validation['valid']) {
×
562
                Log::warning('pinvideo upload rejected: ' . $validation['error'] . ' (user: ' . $uid . ')');
×
563
                return response()->json(['error' => $validation['error']], 422);
×
564
            }
565

566
            Log::debug('storing file');
×
567
            $file_path = "./assets/citizen/" . $safeAddress . "/";
×
568
            if (!file_exists($file_path)) {
×
569
                mkdir($file_path, 0755, true);
×
570
                Log::debug('making dir');
×
571
            }
572

573
            // --- SECURITY: Write .htaccess to prevent PHP execution in upload dir ---
574
            AppHelper::writeUploadHtaccess($file_path);
×
575

576
            $file_path = "./assets/citizen/" . $safeAddress  . "/";
×
577
            $request->file('file')->move($file_path, "profile_video.webm" );
×
578
            $file_path = $file_path . "profile_video.webm";
×
NEW
579
            $hash = AppHelper::upload($file_path, config('blockchain.ipfs.api_url') . "/api/v0/add?pin=true");
×
580
            Log::debug('upload complete: ' . $hash);
×
581
            $citcache = Citizen::where('userid', '=', $uid)->first();
×
582
            if(is_null($citcache)) $citcache = new Citizen;
×
583
            $citcache->userid = $uid;
×
NEW
584
            $citcache->liveness_link = config('blockchain.ipfs.gateway_url').$hash;
×
585
            $citcache->save();
×
586
            Log::debug('saved cit data');
×
587
            return (new Response(json_encode(array("Hash" => $hash)), 200))
×
588
            ->header('Content-Type', "application/json;");
×
589
        }else{
590
            Log::debug('no file found!');
×
591
        }
592

593
        }
594

595
    public function marsAuth(Request $request)
×
596
    {
597
        $this->setCorsHeaders();
×
598

599
        if (!$request->isMethod('post')) {
×
600
            return $this->fastcode(400, "bad request");
×
601
        }
602

603
        $challenge = $request->query('c');
×
604
        $session_string = $request->query('sid');
×
605
        $timestamp = hexdec($request->query('t'));
×
606
        $pubk = $request->input('pubk');
×
607
        $msg = $request->input('msg');
×
608
        $sig = $request->input('sig');
×
609
        $address = $request->input('addr');
×
610

611
        if ($timestamp < (time() - 600)) {
×
612
            return $this->fastcode(408, "request timed out");
×
613
        }
614

615
        $session = MSession::where('sid', $session_string)->first();
×
616

617
        if ($session && in_array($challenge, explode(",", $session->v))) {
×
618
            $marscoinECDSA = new MarscoinECDSA();
×
619
            if ($marscoinECDSA->checkSignatureForMessage($address, $sig, $msg)) {
×
620
                $session->s = $address;
×
621
                $session->save();
×
622
                return $this->fastcode(200, "successfully authenticated");
×
623
            } else {
624
                return $this->fastcode(406, "couldn't verify message");
×
625
            }
626
        } else {
627
            return $this->fastcode(404, "no challenge found");
×
628
        }
629
    }
630

631
    public function checkAuth(Request $request)
×
632
    {
633
        $session_string = $request->query('sid');
×
634
        $session = MSession::where('sid', $session_string)->first();
×
635

636
        $authenticated = (!is_null($session) && !empty($session->s)) ? true : false;
×
637

638
        return response()->json(['sid' => $session_string, 'authenticated' => $authenticated]);
×
639
    }
640

641
    private function fastcode($status, $message)
×
642
    {
643
        return response()->json(["status" => $status, "message" => $message], $status);
×
644
    }
645

646
    private function setCorsHeaders()
×
647
    {
648
        header("Access-Control-Allow-Origin: *");
×
649
        header("Access-Control-Allow-Methods: GET, POST");
×
650
        header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization");
×
651
    }
652

653
    public function wauth(Request $request)
×
654
    {
655
        Log::debug('MarsAuth');
×
656
        $sid = $request->query('sid');
×
657
        Log::debug('MarsAuth');
×
658

659
        if (!empty($sid)) {
×
660
            $mars_session = MSession::where('sid', $sid)->first(); // Retrieve the session using Eloquent
×
661

662
            if ($mars_session && !is_null($mars_session->s) && !empty($mars_session->s)) {
×
663
                // Attempt to retrieve a user via the public address
664
                $citizen = Citizen::where('public_address', $mars_session->s)->first();
×
665

666
                if ($citizen) {
×
667
                    // If a citizen record exists, retrieve the associated user
668
                    $user = User::find($citizen->userid);
×
669
                } else {
670
                    // Create a new user and citizen if no citizen record exists
671
                    $user = User::create([
×
672
                        'email' => $mars_session->s . '@martianrepublic.org',
×
673
                        'password' => Hash::make(Str::random(16)), // Generate a random password
×
674
                    ]);
×
675

676
                    $citizen = new Citizen([
×
677
                        'userid' => $user->id,
×
678
                        'firstname' => 'Unknown', // Set default or use input
×
679
                        'lastname' => 'Uknown',
×
680
                        'public_address' => $mars_session->s,
×
681
                    ]);
×
682

683
                    $citizen->save();
×
684
                }
685

686
                Auth::login($user); // Authenticate the user
×
687
                session()->save(); // Ensure the session is saved immediately
×
688

689
                Log::debug('User authenticated immediately after login: ' . (Auth::check() ? 'true' : 'false'));
×
690
                return redirect('/wallet/dashboard');
×
691
            }
692

693
            return redirect('/login')->withErrors('Could not find User.');
×
694
        } else {
695
            // Handle invalid or expired SID
696
            return redirect('/login')->withErrors('Your session has expired or is invalid.');
×
697
        }
698
    }
699

700

701
    /**
702
         * Handles JSON storage and pinning to a distributed file system.
703
         *
704
         * @hideFromAPIDocumentation
705
         */
706
        public function pinjson(Request $request)
×
707
        {
708
                Log::info("in function");
×
709
                $public_address = $request->input('address');
×
710
                $type = $request->input('type');
×
711
                $json = $request->input('payload');
×
712

713
                // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
714
                $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
715
                if ($safeAddress === null) {
×
716
                        return response()->json(['error' => 'Invalid address format.'], 400);
×
717
                }
718

719
                // --- SECURITY: Sanitize the type parameter to prevent path traversal ---
720
                $safeType = AppHelper::sanitizePathSegment($type);
×
721
                if ($safeType === null) {
×
722
                        return response()->json(['error' => 'Invalid type format.'], 400);
×
723
                }
724

725
                // --- SECURITY: Check file size (max 5MB) ---
726
                if (strlen($json) > 5242880) {
×
727
                        return response()->json(['error' => 'Payload exceeds maximum size of 5MB.'], 422);
×
728
                }
729

730
                // --- SECURITY: Reject payloads containing PHP code ---
731
                if (AppHelper::containsPhpCode($json)) {
×
732
                        Log::warning('pinjson rejected: payload contains PHP code');
×
733
                        return response()->json(['error' => 'Payload contains potentially dangerous content.'], 422);
×
734
                }
735

736
                // --- SECURITY: Validate that payload is valid JSON ---
737
                $decodedJson = json_decode($json);
×
738
                if ($json !== '' && json_last_error() !== JSON_ERROR_NONE) {
×
739
                        return response()->json(['error' => 'Invalid JSON payload.'], 422);
×
740
                }
741

742
                $projectRoot = config('app.project_root', base_path());
×
743
                $base_path =  $projectRoot . "/assets/citizen/" . $safeAddress;
×
744

745
                // Check and create the directory if it doesn't exist
746
                Log::info($base_path);
×
747
                clearstatcache();
×
748
                if (!is_dir($base_path)) {
×
749
                        Log::info("Trying to create directory: " . $base_path);
×
750
                        if (!mkdir($base_path, 0755, true)) {
×
751
                                Log::error("Failed to create directory: " . $base_path);
×
752
                                return response()->json(["error" => "Failed to create directory. Check permissions."], 500);
×
753
                        }
754
                        Log::info("Directory created: " . $base_path);
×
755
                }
756

757
                // Check if the directory is writable, regardless of whether it was just created or already existed
758
                if (!is_writable($base_path)) {
×
759
                        Log::error("Directory not writable: " . $base_path);
×
760
                        return response()->json(["error" => "Directory is not writable. Check permissions."], 500);
×
761
                }
762

763
                // --- SECURITY: Write .htaccess to prevent PHP execution in upload dir ---
764
                AppHelper::writeUploadHtaccess($base_path);
×
765

766
                $file_path = $base_path . "/" . $safeType . ".json";
×
767

768
                // Attempt to write the JSON data to the file
769
                if (file_put_contents($file_path, $json) === false) {
×
770
                        return response()->json(["error" => "Failed to write to file."], 500);
×
771
                }
772

773
                try {
774
                        Log::info("PermaJson: " . $file_path);
×
775

776
                        // Check if the type contains the word 'log'
777
                        if (strpos($safeType, 'log') !== false) {
×
778
                                // The type contains 'log', use uploadFolder
NEW
779
                                $apiResponse = AppHelper::uploadFolder($file_path, config('blockchain.ipfs.api_url') . "/api/v0/add?pin=true&recursive=true&wrap-with-directory=true&quieter");
×
780
                        } else {
781
                                // The type does not contain 'log', use upload
NEW
782
                                $apiResponse = AppHelper::upload($file_path, config('blockchain.ipfs.api_url') . "/api/v0/add?pin=true");
×
783
                        }
784

785
                        if (is_string($apiResponse)) {
×
786
                                $formattedResponse = ['Hash' => $apiResponse];
×
787
                        } else {
788
                                Log::error("Upload error: Formatting");
×
789
                                return response()->json(["error"=>"formatting error"], 500);
×
790
                        }
791

792
                        return response()->json($formattedResponse, 200)->header('Content-Type', "application/json;");
×
793
                } catch (\Exception $e) {
×
794
                        // Handle exceptions during the upload and pinning process
795
                        Log::error("Upload error: " . $e->getMessage());
×
796
                        return response()->json(["error" => $e->getMessage()], 500);
×
797
                }
798
        }
799

800
    public function getThreadsByCategory($categoryId) {
×
801
        $threads = $this->fetchThreads($categoryId);
×
802
        return response()->json(['threads' => $threads]);
×
803
    }
804

805
    // private function fetchThreads($categoryId) {
806
    //     $threads = DB::table('forum_threads')
807
    //         ->where('forum_threads.category_id', $categoryId)
808
    //         ->leftJoin('users', 'forum_threads.author_id', '=', 'users.id')
809
    //         ->leftJoin('profile', 'users.id', '=', 'profile.userid')
810
    //         ->select(
811
    //             'forum_threads.id',
812
    //             'forum_threads.title',
813
    //             'forum_threads.created_at',
814
    //             'forum_threads.reply_count',
815
    //             'users.fullname as author_name'
816
    //         )
817
    //         ->orderBy('forum_threads.created_at', 'desc')
818
    //         ->get();
819
    //     return $threads;
820
    // }
821

822
    private function fetchThreads($categoryId) {
×
823
        $userId = Auth::id();
×
824

825
        $threads = DB::table('forum_threads')
×
826
            ->where('forum_threads.category_id', $categoryId)
×
827
            ->leftJoin('users', 'forum_threads.author_id', '=', 'users.id')
×
828
            ->leftJoin('profile', 'users.id', '=', 'profile.userid')
×
829
            ->leftJoin('user_blocks as ub', function($join) use ($userId) {
×
830
                $join->on('forum_threads.author_id', '=', 'ub.blocked_user_id')
×
831
                     ->where('ub.user_id', '=', $userId);
×
832
            })
×
833
            ->select(
×
834
                'forum_threads.id',
×
835
                'forum_threads.title',
×
836
                'forum_threads.created_at',
×
837
                'forum_threads.reply_count',
×
838
                'users.fullname as author_name',
×
839
                DB::raw('IF(ub.blocked_user_id IS NOT NULL, true, false) as is_blocked')
×
840
            )
×
841
            ->orderBy('forum_threads.created_at', 'desc')
×
842
            ->get();
×
843

844
        return $threads;
×
845
    }
846

847

848
    public function getThreadComments($threadId) {
×
849
        // Assume $threadId is passed correctly to the function
850
        $comments = $this->fetchCommentsByThread($threadId);
×
851
        return response()->json(['comments' => $comments]);
×
852
    }
853

854
    // private function fetchCommentsByThread($threadId) {
855
    //     // The query as outlined above
856
    //     // Return the collection of comments
857
    //     $query = "
858
    //         WITH RECURSIVE CommentTree AS (
859
    //             SELECT
860
    //                 p.id,
861
    //                 p.thread_id,
862
    //                 p.author_id,
863
    //                 p.content,
864
    //                 p.post_id as pid,
865
    //                 p.created_at,
866
    //                 CHAR_LENGTH(p.content) as char_length_sum
867
    //             FROM
868
    //                 forum_posts p
869
    //             WHERE
870
    //                 p.thread_id = ? AND p.post_id IS NULL
871

872
    //             UNION ALL
873

874
    //             SELECT
875
    //                 p.id,
876
    //                 p.thread_id,
877
    //                 p.author_id,
878
    //                 p.content,
879
    //                 p.post_id,
880
    //                 p.created_at,
881
    //                 ct.char_length_sum + CHAR_LENGTH(p.content)
882
    //             FROM
883
    //                 forum_posts p
884
    //             INNER JOIN
885
    //                 CommentTree ct ON p.post_id = ct.id
886
    //         )
887
    //         SELECT
888
    //             ct.id,
889
    //             ct.thread_id,
890
    //             ct.author_id,
891
    //             u.fullname,
892
    //             ct.content,
893
    //             ct.created_at,
894
    //             ct.pid,
895
    //             CHAR_LENGTH(ct.content) as char_length_sum
896
    //         FROM
897
    //             CommentTree ct
898
    //         LEFT JOIN users u ON ct.author_id = u.id
899
    //         LEFT JOIN profile pr ON ct.author_id = pr.userid
900
    //         ORDER BY
901
    //             ct.pid ASC,
902
    //             ct.created_at ASC;
903
    //     ";
904

905
    //     $comments = DB::select($query, [$threadId]);
906
    //     $commentsCollection = collect($comments);
907

908
    //     return response()->json(['comments' => $commentsCollection]);
909
    // }
910

911
    private function fetchCommentsByThread($threadId) {
×
912
        $userId = Auth::id();
×
913

914
        $query = "
×
915
            WITH RECURSIVE CommentTree AS (
916
                SELECT
917
                    p.id,
918
                    p.thread_id,
919
                    p.author_id,
920
                    p.content,
921
                    p.post_id as pid,
922
                    p.created_at,
923
                    CHAR_LENGTH(p.content) as char_length_sum
924
                FROM
925
                    forum_posts p
926
                WHERE
927
                    p.thread_id = ? AND p.post_id IS NULL
928

929
                UNION ALL
930

931
                SELECT
932
                    p.id,
933
                    p.thread_id,
934
                    p.author_id,
935
                    p.content,
936
                    p.post_id,
937
                    p.created_at,
938
                    ct.char_length_sum + CHAR_LENGTH(p.content)
939
                FROM
940
                    forum_posts p
941
                INNER JOIN
942
                    CommentTree ct ON p.post_id = ct.id
943
            )
944
            SELECT
945
                ct.id,
946
                ct.thread_id,
947
                ct.author_id,
948
                u.fullname,
949
                ct.content,
950
                ct.created_at,
951
                ct.pid,
952
                CHAR_LENGTH(ct.content) as char_length_sum,
953
                IF(ub.blocked_user_id IS NOT NULL, true, false) as is_blocked
954
            FROM
955
                CommentTree ct
956
            LEFT JOIN users u ON ct.author_id = u.id
957
            LEFT JOIN profile pr ON ct.author_id = pr.userid
958
            LEFT JOIN user_blocks ub ON ub.blocked_user_id = ct.author_id AND ub.user_id = ?
959
            ORDER BY
960
                ct.pid ASC,
961
                ct.created_at ASC;
962
        ";
×
963

964
        $comments = DB::select($query, [$threadId, $userId]);
×
965
        $commentsCollection = collect($comments);
×
966
        return response()->json(['comments' => $commentsCollection]);
×
967
    }
968

969

970
    public function getAllCategoriesWithThreads() {
×
971
        $userId = Auth::id();
×
972

973
        $categories = DB::table('forum_categories')->get();
×
974

975
        // Fetch all threads in a single query instead of N+1
976
        $threads = DB::table('forum_threads')
×
977
            ->leftJoin('users', 'forum_threads.author_id', '=', 'users.id')
×
978
            ->leftJoin('profile', 'users.id', '=', 'profile.userid')
×
979
            ->leftJoin('user_blocks as ub', function($join) use ($userId) {
×
980
                $join->on('forum_threads.author_id', '=', 'ub.blocked_user_id')
×
981
                     ->where('ub.user_id', '=', $userId);
×
982
            })
×
983
            ->select(
×
984
                'forum_threads.id',
×
985
                'forum_threads.category_id',
×
986
                'forum_threads.title',
×
987
                'forum_threads.created_at',
×
988
                'forum_threads.reply_count',
×
989
                'users.fullname as author_name',
×
990
                DB::raw('IF(ub.blocked_user_id IS NOT NULL, true, false) as is_blocked')
×
991
            )
×
992
            ->orderBy('forum_threads.created_at', 'desc')
×
993
            ->get()
×
994
            ->groupBy('category_id');
×
995

996
        foreach ($categories as $category) {
×
997
            $category->threads = $threads->get($category->id, collect());
×
998
        }
999

1000
        return response()->json(['categories' => $categories]);
×
1001
    }
1002

1003

1004
    public function createThread(Request $request)
×
1005
    {
1006
        $request->validate([
×
1007
            'category_id' => 'required|exists:forum_categories,id',
×
1008
            'title' => 'required|string|max:255',
×
1009
            'content' => 'required|string',
×
1010
        ]);
×
1011

1012
        $thread = new Threads();
×
1013
        $thread->category_id = $request->category_id;
×
1014
        $thread->author_id = Auth::id();
×
1015
        $thread->title = $request->title;
×
1016
        $thread->save();
×
1017

1018
        $post = new Posts();
×
1019
        $post->thread_id = $thread->id;
×
1020
        $post->author_id = Auth::id();
×
1021
        $post->content = $request->content;
×
1022
        $post->save();
×
1023

1024
        $thread->first_post_id = $post->id;
×
1025
        $thread->last_post_id = $post->id;
×
1026
        $thread->reply_count = 0;
×
1027
        $thread->save();
×
1028

1029
        return response()->json([
×
1030
            'message' => 'Thread created successfully',
×
1031
            'thread_id' => $thread->id,
×
1032
            'post_id' => $post->id,
×
1033
        ], 201);
×
1034
    }
1035

1036
    public function createComment(Request $request, $threadId)
×
1037
    {
1038
        $request->validate([
×
1039
            'content' => 'required|string',
×
1040
            'post_id' => 'nullable|exists:forum_posts,id',
×
1041
        ]);
×
1042

1043
        $thread = Threads::findOrFail($threadId);
×
1044

1045
        $post = new Posts();
×
1046
        $post->thread_id = $threadId;
×
1047
        $post->author_id = Auth::id();
×
1048
        $post->content = $request->content;
×
1049
        $post->post_id = $request->post_id; // This will be null for top-level comments
×
1050
        $post->save();
×
1051

1052
        $thread->last_post_id = $post->id;
×
1053
        $thread->reply_count += 1;
×
1054
        $thread->save();
×
1055

1056
        return response()->json([
×
1057
            'message' => 'Comment created successfully',
×
1058
            'post_id' => $post->id,
×
1059
        ], 201);
×
1060
    }
1061

1062

1063
    public function blockUser(Request $request, $id)
×
1064
    {
1065
        Log::debug("in function");
×
1066
        $uid = Auth::user()->id;
×
1067
        Log::debug("auth happened");
×
1068
        $blockedUserId = $id;
×
1069

1070
        // Insert into `user_blocks` table if not already blocked
1071
        DB::table('user_blocks')->updateOrInsert(
×
1072
            ['user_id' => $uid, 'blocked_user_id' => $blockedUserId]
×
1073
        );
×
1074

1075
        return response()->json(['message' => 'User blocked successfully'], 201);
×
1076
    }
1077

1078
    public function handleEula(Request $request)
×
1079
    {
1080
        $uid = Auth::user()->id;
×
1081
        $profile = Profile::where('userid', $uid)->firstOrFail();
×
1082

1083
        return response()->json([
×
1084
            'is_signed' => (bool)$profile->signed_eula
×
1085
        ]);
×
1086
    }
1087

1088
    public function setEula(Request $request)
×
1089
    {
1090
        $uid = Auth::user()->id;
×
1091
        $profile = Profile::where('userid', $uid)->firstOrFail();
×
1092
        if (!$profile->signed_eula) {
×
1093
            $profile->signed_eula = 1;
×
1094
            $profile->save();
×
1095
        }
1096
        return response()->json([
×
1097
            'message' => 'EULA signed successfully',
×
1098
            'is_signed' => true
×
1099
        ]);
×
1100
    }
1101

1102

1103
    public function deleteUser(Request $request, $id)
×
1104
    {
1105
        try {
1106
            // Users can only deactivate their own account
1107
            $authUser = Auth::user();
×
1108
            if ((int) $authUser->id !== (int) $id) {
×
1109
                return response()->json(['message' => 'Unauthorized: you can only delete your own account.'], 403);
×
1110
            }
1111

1112
            $user = User::findOrFail($id);
×
1113

1114
            Log::debug("Deleting... " . $id);
×
1115

1116
            $user->update([
×
1117
                'status' => 'inactive'
×
1118
            ]);
×
1119
            Log::debug("Status updated");
×
1120

1121
            // Revoke all tokens
1122
            if (method_exists($user, 'tokens')) {
×
1123
                $user->tokens()->delete();
×
1124
            }
1125
            Log::debug("Token wiped...");
×
1126

1127
            return response()->json([
×
1128
                'message' => 'User deleted successfully'
×
1129
            ], 200);
×
1130

1131

1132
        } catch (ModelNotFoundException $e) {
×
1133
            return response()->json([
×
1134
                'message' => 'User not found'
×
1135
            ], 404);
×
1136
        } catch (\Exception $e) {
×
1137
            \Log::error('Error deleting user: ' . $e->getMessage());
×
1138
            return response()->json([
×
1139
                'message' => 'An error occurred while deleting the user'
×
1140
            ], 500);
×
1141
        }
1142
    }
1143

1144

1145

1146

1147
}
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