Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion key-value-versions-app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

PASSPORT_PASSWORD_CLIENT_ID=01989503-1ce8-735f-a487-0729ef4f1d1b
PASSPORT_PASSWORD_SECRET=WqkuICsupCfBL1xLt71TfBSaaLVlaiTd0iMSudpu
PASSPORT_PASSWORD_CLIENT_SECRET=WqkuICsupCfBL1xLt71TfBSaaLVlaiTd0iMSudpu
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\Rules;

class AuthController extends Controller
Expand All @@ -31,56 +30,6 @@ public function register(Request $request): JsonResponse
return response()->json(['message' => 'User registered successfully'], 201);
}

public function login(Request $request): JsonResponse
{
$request->validate([
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
]);

if (!Auth::attempt($request->only('email', 'password'))) {
return response()->json(['message' => 'Invalid credentials'], 401);
}

$response = Http::asForm()->post(env('APP_URL') . '/oauth/token', [
'grant_type' => 'password',
'client_id' => env('PASSPORT_PASSWORD_CLIENT_ID'),
'client_secret' => env('PASSPORT_PASSWORD_SECRET'),
'username' => $request->email,
'password' => $request->password,
'scope' => '*',
]);

return response()->json([
'user' => Auth::user(),
'token' => $response->json(),
]);
}

public function me(): JsonResponse
{
$user = auth()->user();

return response()->json(['user' => $user]);
}

public function refreshToken(Request $request): JsonResponse
{
$formData = $request->validate([
'refresh_token' => ['required', 'string'],
]);

$response = Http::asForm()->post(env('APP_URL') . '/oauth/token', [
'grant_type' => 'refresh_token',
'refresh_token' => $formData->refresh_token,
'client_id' => env('PASSPORT_PASSWORD_CLIENT_ID'),
'client_secret' => env('PASSPORT_PASSWORD_SECRET'),
'scope' => '*',
]);

return response()->json(['token' => $response->json()]);
}

public function logout(): JsonResponse
{
Auth::user()->tokens()->delete();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Repositories\KeyValueVersionRepository;
use Carbon\Carbon;
use Illuminate\Http\Request;

class KeyValueVersionController extends Controller
{
protected KeyValueVersionRepository $repository;

public function __construct(KeyValueVersionRepository $repository)
{
$this->repository = $repository;
}

/**
* Store a newly created resource in storage.
* This method handles POST /api/v1/keys
*/
public function store(Request $request)
{
$request->validate([
'key' => 'required|string|max:255',
'value' => 'required',
]);

$hash = hash('sha256', $request->value);

$current = $this->repository->getLatestByKey($request->key);

if ($current && $current->value_hash === $hash) {
return response()->json(
[
'key' => $current->key,
'value' => json_decode($current->value, true) ?? $current->value,
'createdAt' => $current->created_at->timestamp,
]
);
}

$new = $this->repository->create(
[
'key' => $request->key,
'value' => $request->value,
'value_hash' => $hash,
'created_at' => Carbon::now('UTC'),
]
);

return response()->json(
[
'key' => $new->key,
'value' => json_decode($new->value, true) ?? $new->value,
'createdAt' => $new->created_at->timestamp,
],
201,
);
}

/**
* Display a listing of all currently stored keys with their latest values.
* This method handles GET /api/v1/keys
*/
public function index()
{
$all = $this->repository->getAllLatestKeys();

$formatted = $all->map(function ($latest) {
return [
'key' => $latest->key,
'value' => json_decode($latest->value, true) ?? $latest->value,
'createdAt' => $latest->created_at->timestamp,
];
});

return response()->json($formatted);
}

/**
* Display the specified resource (latest value or value at timestamp).
* This method handles GET /v1/keys/{key} and /v1/keys/{key}?timestamp={unix_timestamp}
*/
public function show(Request $request, string $key)
{
if (! $request->has('timestamp') || ! is_numeric($request->query('timestamp'))) {
$latest = $this->repository->getLatestByKey($key);
} else {
$timestamp = Carbon::createFromTimestamp((int) $request->query('timestamp'), 'UTC');
$latest = $this->repository->getLatestByKeyAndTimestamp($key, $timestamp);
}

if ($latest === null) {
return response()->json(['error' => 'Key not found or no version at specified timestamp'], 404);
}

return response()->json([
'key' => $latest->key,
'value' => json_decode($latest->value, true) ?? $latest->value,
'createdAt' => $latest->created_at->timestamp,
]);
}

/**
* Display all values (versions) for a key.
* This method handles GET /v1/keys/{key}/history
*/
public function history(string $key)
{
$all = $this->repository->getAllByKey($key);

if ($all->isEmpty()) {
return response()->json(['error' => 'Key not found'], 404);
}

$formatted = $all->map(function ($version) {
return [
'key' => $version->key,
'value' => json_decode($version->value, true) ?? $version->value,
'createdAt' => $version->created_at->timestamp,
];
});

return response()->json($formatted);
}
}
3 changes: 3 additions & 0 deletions key-value-versions-app/app/Models/KeyValueVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class KeyValueVersion extends Model
{
protected $table = 'key_value_versions';

use HasFactory;

protected $fillable = [
'key',
'value',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace App\Repositories;

use App\Models\KeyValueVersion;
use Illuminate\Support\Facades\DB;

interface KeyValueVersionRepositoryInterface
{
public function create(array $data);

public function getLatestByKey(string $key);

public function getAllLatestKeys();

public function getAllByKey(string $key);
}

class KeyValueVersionRepository implements KeyValueVersionRepositoryInterface
{
protected KeyValueVersion $model;

public function __construct(KeyValueVersion $model)
{
$this->model = $model;
}

public function getAllLatestKeys()
{
return $this->model
->whereIn('id', function ($query) {
$query->select(DB::raw('MAX(id)'))
->from('key_value_versions')
->groupBy('key');
})->get();
}

public function create(array $data): KeyValueVersion
{
return $this->model->create($data);
}

public function getLatestByKey(string $key): ?KeyValueVersion
{
return $this->model
->where('key', $key)
->orderByDesc('created_at')
->orderByDesc('id')
->first();
}

public function getAllByKey(string $key)
{
return $this->model
->where('key', $key)
->orderByDesc('created_at')
->orderByDesc('id')
->get();
}

public function getLatestByKeyAndTimestamp(string $key, $timestamp): ?KeyValueVersion
{
return $this->model
->where('key', $key)
->where('created_at', '<=', $timestamp)
->orderByDesc('created_at')
->orderByDesc('id')
->first();
}
}
6 changes: 3 additions & 3 deletions key-value-versions-app/bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
api: __DIR__ . '/../routes/api.php',
commands: __DIR__ . '/../routes/console.php',
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
apiPrefix: 'api/v1',
)
Expand Down
13 changes: 13 additions & 0 deletions key-value-versions-app/config/passport.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@

'public_key' => env('PASSPORT_PUBLIC_KEY'),

/*
|--------------------------------------------------------------------------
| Client ID and Secret
|--------------------------------------------------------------------------
|
| Client ID and Secret
|
*/

'client_id' => env('PASSPORT_PASSWORD_CLIENT_ID'),

'client_secret' => env('PASSPORT_PASSWORD_CLIENT_SECRET'),

/*
|--------------------------------------------------------------------------
| Passport Database Connection
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Database\Factories;

use App\Models\KeyValueVersion;
use Illuminate\Database\Eloquent\Factories\Factory;

class KeyValueVersionFactory extends Factory
{
protected $model = KeyValueVersion::class;

public function definition()
{
$value = json_encode(['data' => $this->faker->word]);

return [
'key' => $this->faker->word,
'value' => $value,
'value_hash' => hash('sha256', $value),
'created_at' => now(),
];
}
}
12 changes: 7 additions & 5 deletions key-value-versions-app/routes/api.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<?php

use App\Http\Controllers\Api\V1\AuthController;
use App\Http\Controllers\Api\V1\KeyValueVersionController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;


Route::post('/login', [AuthController::class, 'login']);
Route::post('/refresh', [AuthController::class, 'refreshToken']);

Route::post('/register', [AuthController::class, 'register']);

Route::group(['middleware' => ['auth:api']], function () {
Route::post('/logout', [AuthController::class, 'logout']);

Route::get('/user', function (Request $request) {
return $request->user();
});

Route::post('/logout', [AuthController::class, 'logout']);
Route::post('/keys', [KeyValueVersionController::class, 'store']);
Route::get('/keys', [KeyValueVersionController::class, 'index']);
Route::get('/keys/{key}', [KeyValueVersionController::class, 'show']);
Route::get('/keys/{key}/history', [KeyValueVersionController::class, 'history']);
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@

$this->assertGuest();
$response->assertRedirect('/');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@
$this->actingAs($user)->get($verificationUrl);

expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@
]);

$response->assertSessionHasErrors();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@

return true;
});
});
});
Loading
Loading