Skip to content

Commit 0f133b5

Browse files
authored
CLI support: GitHub token storage, completion validation, local dev (#184)
* Store GitHub token on login for CLI fork support - Add github_token column to users table - Store GitHub OAuth token when user logs in via GitHub - Expose github_token in UserResource when include_github_token=1 is requested - Token is hidden by default (in $hidden array) * Validate submission before completion, skip screenshot locally - Block marking challenge as complete without a submission (422) - Skip screenshot service when SCREENSHOT_SERVICE_BASE_URL is empty (allows local development without the screenshot service) - Same change applied to both submit and updateSubmission methods * Add tests for completion validation - Test that completing without submission returns 422 - Test that completing after submission succeeds
1 parent ac3821e commit 0f133b5

6 files changed

Lines changed: 117 additions & 28 deletions

File tree

app/Http/Controllers/Auth/GithubLoginController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public function githubLogin(Request $request)
6868
$user->github_id = $githubUserData->getId();
6969
$user->github_user = $githubUserData->getNickname();
7070
$user->github_data = $githubUserData->user;
71+
$user->github_token = $request->input('github_token');
7172

7273
$user->save();
7374

app/Http/Controllers/ChallengeController.php

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,17 @@ public function updateChallengeUser(Request $request, $slug)
249249
'fork_url' => 'nullable|url',
250250
]);
251251

252+
// Prevent marking as complete without a submission
253+
if (! empty($validated['completed']) && $validated['completed'] === true) {
254+
$pivot = $challenge->users()->where('user_id', $request->user()->id)->first();
255+
if ($pivot && ! $pivot->pivot->submitted_at) {
256+
return response()->json(
257+
['error' => 'You must submit your solution before marking as complete.'],
258+
422
259+
);
260+
}
261+
}
262+
252263
$challenge
253264
->users()
254265
->updateExistingPivot($request->user()->id, $validated);
@@ -325,22 +336,26 @@ public function submit(Request $request, $slug)
325336
"challenge-screenshots/$slug-$challengeUser->github_id-".
326337
Str::random(10).
327338
'.webp';
328-
$apiUrl = config('services.screenshot.base_url').'/screenshot';
329339

330-
$response = Http::withHeaders([
331-
'Authorization' => 'Bearer '.config('services.screenshot.token'),
332-
'Accept' => 'application/json',
333-
])->post($apiUrl, [
334-
'url' => $validated['submission_url'],
335-
'fileName' => $imagePath,
336-
]);
340+
$imageUrl = null;
341+
$apiUrl = config('services.screenshot.base_url');
337342

338-
if ($response->failed()) {
339-
abort(500, $response);
340-
}
343+
if ($apiUrl) {
344+
$response = Http::withHeaders([
345+
'Authorization' => 'Bearer '.config('services.screenshot.token'),
346+
'Accept' => 'application/json',
347+
])->post($apiUrl.'/screenshot', [
348+
'url' => $validated['submission_url'],
349+
'fileName' => $imagePath,
350+
]);
341351

342-
$data = $response->json();
343-
$imageUrl = $data['imageUrl'];
352+
if ($response->failed()) {
353+
abort(500, $response);
354+
}
355+
356+
$data = $response->json();
357+
$imageUrl = $data['imageUrl'];
358+
}
344359

345360
// Saves in DB
346361
$challengeUser->pivot->submission_url = $validated['submission_url'];
@@ -472,24 +487,28 @@ public function updateSubmission(Request $request, $slug)
472487
"challenge-screenshots/$slug-$challengeUser->github_id-".
473488
Str::random(10).
474489
'.webp';
475-
$apiUrl = config('services.screenshot.base_url').'/screenshot';
476490

477-
$response = Http::withHeaders([
478-
'Authorization' => 'Bearer '.config('services.screenshot.token'),
479-
'Accept' => 'application/json',
480-
])->put($apiUrl, [
481-
'url' => $validated['submission_url'],
482-
'fileName' => $imagePath,
483-
'oldFilename' => $challengeUser->pivot->submission_image_url,
484-
]);
491+
$s3Location = $challengeUser->pivot->submission_image_url;
492+
$apiUrl = config('services.screenshot.base_url');
493+
494+
if ($apiUrl) {
495+
$response = Http::withHeaders([
496+
'Authorization' => 'Bearer '.config('services.screenshot.token'),
497+
'Accept' => 'application/json',
498+
])->put($apiUrl.'/screenshot', [
499+
'url' => $validated['submission_url'],
500+
'fileName' => $imagePath,
501+
'oldFilename' => $challengeUser->pivot->submission_image_url,
502+
]);
485503

486-
if ($response->failed()) {
487-
$error = $response->json();
488-
abort(500, 'API request failed: '.$error['message']);
489-
}
504+
if ($response->failed()) {
505+
$error = $response->json();
506+
abort(500, 'API request failed: '.$error['message']);
507+
}
490508

491-
$data = $response->json();
492-
$s3Location = $data['imageUrl'];
509+
$data = $response->json();
510+
$s3Location = $data['imageUrl'];
511+
}
493512

494513
// Saves in DB
495514
$challengeUser->pivot->submission_url = $validated['submission_url'];

app/Http/Resources/UserResource.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function toArray(Request $request): array
2727
'created_at' => $this->created_at,
2828
'settings' => $this->settings,
2929
'avatar' => new UserAvatarResource($this),
30+
'github_token' => $this->when($request->boolean('include_github_token'), $this->github_token),
3031
];
3132
}
3233
}

app/Models/User.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class User extends Authenticatable
4040
'remember_token',
4141
'discord_data',
4242
'github_data',
43+
'github_token',
4344
];
4445

4546
protected $casts = [
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::table('users', function (Blueprint $table) {
12+
$table->text('github_token')->nullable()->after('github_data');
13+
});
14+
}
15+
16+
public function down(): void
17+
{
18+
Schema::table('users', function (Blueprint $table) {
19+
$table->dropColumn('github_token');
20+
});
21+
}
22+
};

tests/Feature/ChallengeSubmissionTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,49 @@ public function it_returns_participant_avatars(): void
232232
]);
233233
$this->assertEquals(5, $response->json('count'));
234234
}
235+
236+
/** @test */
237+
public function it_prevents_marking_complete_without_submission(): void
238+
{
239+
Event::fake();
240+
Mail::fake();
241+
242+
$user = User::factory()->create();
243+
$challenge = Challenge::factory()->create(['status' => 'published']);
244+
$challenge->users()->attach($user->id);
245+
246+
$response = $this->signIn($user)
247+
->putJson("/api/challenges/{$challenge->slug}", [
248+
'completed' => true,
249+
]);
250+
251+
$response->assertStatus(422);
252+
$response->assertJson(['error' => 'You must submit your solution before marking as complete.']);
253+
}
254+
255+
/** @test */
256+
public function it_allows_marking_complete_after_submission(): void
257+
{
258+
Event::fake();
259+
Mail::fake();
260+
261+
$user = User::factory()->create();
262+
$challenge = Challenge::factory()->create(['status' => 'published']);
263+
$challenge->users()->attach($user->id, [
264+
'submitted_at' => now(),
265+
'submission_url' => 'https://example.com',
266+
]);
267+
268+
$response = $this->signIn($user)
269+
->putJson("/api/challenges/{$challenge->slug}", [
270+
'completed' => true,
271+
]);
272+
273+
$response->assertOk();
274+
275+
$this->assertEquals(
276+
1,
277+
$challenge->users()->where('user_id', $user->id)->first()->pivot->completed
278+
);
279+
}
235280
}

0 commit comments

Comments
 (0)