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
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,18 @@ After cloning the repository, you need to perform the following steps to get the
```bash
php artisan key:generate
```

## Manual Feature Implementation

During the implementation of the user and suggestion systems, the agent encountered significant and persistent issues with the Docker-based development environment that prevented the use of `docker compose`, `composer`, and `php artisan` commands. The issues included Docker daemon permission errors, Docker Hub rate limiting, and `git` ownership errors within the `run_in_bash_session` tool.

As a workaround, and per user instruction, the features were implemented manually by creating and editing the necessary files directly. This includes all migrations, models, controllers, views, routes, and middleware for the user authentication, role-based access control, suggestion system, and admin user management features.

### Corrected Docker Configuration

Although the Docker environment could not be run, several issues within the Docker configuration files were identified and corrected to aid future development:
- The `compose.dev.yml` file was updated to use the official Docker Hub images for `nginx`, `postgres`, and `redis` instead of the incorrect `public.ecr.aws` prefixed images.
- The `docker/common/php-fpm/Dockerfile` and `docker/development/workspace/Dockerfile` files were updated to use the correct official base images for `php` and `composer`.
- The package names for Alpine Linux dependencies in the `apk add` commands within the Dockerfiles were corrected (e.g., `libssl-dev` to `openssl-dev`).

It is recommended that the environment issues be resolved to allow for proper testing and execution of the application.
77 changes: 77 additions & 0 deletions app/Http/Controllers/Admin/UserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class UserController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'role:' . User::ROLE_ADMIN]);

Check failure on line 14 in app/Http/Controllers/Admin/UserController.php

View workflow job for this annotation

GitHub Actions / test

Call to an undefined method App\Http\Controllers\Admin\UserController::middleware().
}

public function index()
{
$users = User::all();
return view('admin.users.index', compact('users'));
}

public function create()
{
return view('admin.users.create');
}

public function store(Request $request)
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'role' => ['required', 'string', 'in:' . User::ROLE_ADMIN . ',' . User::ROLE_WORKER . ',' . User::ROLE_USER],
]);

User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'role' => $request->role,
]);

return redirect()->route('admin.users.index')->with('success', 'User created successfully.');
}

public function edit(User $user)
{
return view('admin.users.edit', compact('user'));
}

public function update(Request $request, User $user)
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id],
'role' => ['required', 'string', 'in:' . User::ROLE_ADMIN . ',' . User::ROLE_WORKER . ',' . User::ROLE_USER],
]);

$user->update($request->only('name', 'email', 'role'));

if ($request->filled('password')) {
$request->validate([
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
$user->update(['password' => Hash::make($request->password)]);
}

return redirect()->route('admin.users.index')->with('success', 'User updated successfully.');
}

public function destroy(User $user)
{
$user->delete();
return redirect()->route('admin.users.index')->with('success', 'User deleted successfully.');
}
}
44 changes: 44 additions & 0 deletions app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
public function create()
{
return view('auth.login');
}

public function store(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);

if (Auth::attempt($credentials)) {
$request->session()->regenerate();

return redirect()->intended('home');
}

return back()->withErrors([
'email' => 'The provided credentials do not match our records.',
]);
}

public function destroy(Request $request)
{
Auth::logout();

$request->session()->invalidate();

$request->session()->regenerateToken();

return redirect('/');
}
}
36 changes: 36 additions & 0 deletions app/Http/Controllers/Auth/RegisterController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;

class RegisterController extends Controller
{
public function create()
{
return view('auth.register');
}

public function store(Request $request)
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);

$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);

Auth::login($user);

return redirect()->route('home');
}
}
99 changes: 99 additions & 0 deletions app/Http/Controllers/SuggestionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace App\Http\Controllers;

use App\Models\Suggestion;
use App\Models\LexicalEntry;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class SuggestionController extends Controller
{
public function __construct()
{
$this->middleware('auth');

Check failure on line 14 in app/Http/Controllers/SuggestionController.php

View workflow job for this annotation

GitHub Actions / test

Call to an undefined method App\Http\Controllers\SuggestionController::middleware().
}

public function index()
{
// Add authorization logic here later (only for admins/workers)
$suggestions = Suggestion::with('user')->latest()->get();
return view('suggestions.index', compact('suggestions'));
}

public function create(Request $request)
{
$model_type = $request->get('model_type');
$model_id = $request->get('model_id');
$type = $request->get('type');

$model = null;
if ($model_id) {
$model = $model_type::find($model_id);
}

return view('suggestions.create', compact('model_type', 'model_id', 'type', 'model'));
}

public function store(Request $request)
{
$request->validate([
'type' => ['required', 'string', 'in:create,update,delete'],
'model_type' => ['required', 'string'],
'model_id' => ['nullable', 'integer'],
'data' => ['required', 'array'],
]);

Suggestion::create([
'user_id' => Auth::id(),
'type' => $request->type,
'model_type' => $request->model_type,
'model_id' => $request->model_id,
'data' => $request->data,
]);

return redirect()->route('home')->with('success', 'Your suggestion has been submitted for review.');
}

public function show(Suggestion $suggestion)
{
return view('suggestions.show', compact('suggestion'));
}

public function approve(Suggestion $suggestion)
{
// Authorization logic will be added later using middleware
// $this->authorize('approve', $suggestion);

$modelClass = $suggestion->model_type;
$data = $suggestion->data;

switch ($suggestion->type) {
case 'create':
$modelClass::create($data);
break;
case 'update':
$model = $modelClass::find($suggestion->model_id);
$model->update($data);
break;
case 'delete':
$model = $modelClass::find($suggestion->model_id);
$model->delete();
break;
}

$suggestion->update(['status' => 'approved']);

return redirect()->route('suggestions.index')->with('success', 'Suggestion approved.');
}

public function reject(Suggestion $suggestion)
{
// Authorization logic will be added later using middleware
// $this->authorize('reject', $suggestion);

$suggestion->update(['status' => 'rejected']);

return redirect()->route('suggestions.index')->with('success', 'Suggestion rejected.');
}
}
35 changes: 35 additions & 0 deletions app/Http/Middleware/CheckRole.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class CheckRole
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $role
* @return mixed
*/
public function handle(Request $request, Closure $next, ...$roles)

Check failure on line 19 in app/Http/Middleware/CheckRole.php

View workflow job for this annotation

GitHub Actions / test

PHPDoc tag @param references unknown parameter: $role
{
if (!Auth::check()) {
return redirect('login');
}

$user = Auth::user();

foreach ($roles as $role) {
if ($user->role === $role) {
return $next($request);
}
}

abort(403, 'Unauthorized action.');
}
}
36 changes: 36 additions & 0 deletions app/Models/Suggestion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Suggestion extends Model
{
use HasFactory;

protected $fillable = [
'user_id',
'type',
'model_type',
'model_id',
'data',
'status',
];

protected $casts = [
'data' => 'array',
];

public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

public function model(): MorphTo
{
return $this->morphTo();
}
}
5 changes: 5 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class User extends Authenticatable
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;

public const ROLE_ADMIN = 'admin';
public const ROLE_WORKER = 'worker';
public const ROLE_USER = 'user';

/**
* The attributes that are mass assignable.
*
Expand All @@ -21,6 +25,7 @@ class User extends Authenticatable
'name',
'email',
'password',
'role',
];

/**
Expand Down
4 changes: 4 additions & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '*');

$middleware->alias([
'role' => \App\Http\Middleware\CheckRole::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
Expand Down
Loading
Loading