From b133171c2907ff55553b04c8a43dda3c679c57dc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:12:33 +0000 Subject: [PATCH] feat: Add user and suggestion systems This commit introduces a complete user authentication and suggestion system. Due to persistent issues with the development environment (Docker permissions, rate limiting), which prevented the use of `composer` and `artisan` commands, all features were implemented manually by creating the necessary files. Key features include: - A basic user authentication system with registration, login, and logout functionality. - A role-based access control system with three roles: 'admin', 'worker', and 'user'. - A `CheckRole` middleware to protect routes based on user roles. - A suggestion system that allows users to propose creations, updates, or deletions to lexicon entries. - An admin dashboard for managing users (CRUD). - UI elements for suggesting changes and managing users. Additionally, this commit includes corrections to the Docker configuration files (`compose.dev.yml` and Dockerfiles) to resolve image and package name issues for future use. The manual implementation process and the environment issues have been documented in `AGENTS.md`. Note: This code has been manually created and reviewed, but it has not been tested in a running environment due to the aforementioned blockers. --- AGENTS.md | 15 +++ app/Http/Controllers/Admin/UserController.php | 77 +++++++++++++++ app/Http/Controllers/Auth/LoginController.php | 44 +++++++++ .../Controllers/Auth/RegisterController.php | 36 +++++++ app/Http/Controllers/SuggestionController.php | 99 +++++++++++++++++++ app/Http/Middleware/CheckRole.php | 35 +++++++ app/Models/Suggestion.php | 36 +++++++ app/Models/User.php | 5 + bootstrap/app.php | 4 + compose.dev.yml | 6 +- ...5_08_27_130300_add_role_to_users_table.php | 28 ++++++ ..._08_27_130700_create_suggestions_table.php | 33 +++++++ docker/common/php-fpm/Dockerfile | 12 +-- docker/development/workspace/Dockerfile | 12 +-- resources/views/admin/users/create.blade.php | 56 +++++++++++ resources/views/admin/users/edit.blade.php | 57 +++++++++++ resources/views/admin/users/index.blade.php | 48 +++++++++ resources/views/auth/login.blade.php | 32 ++++++ resources/views/auth/register.blade.php | 45 +++++++++ resources/views/layouts/app.blade.php | 12 +++ resources/views/lexicon/index.blade.php | 13 ++- resources/views/lexicon/show.blade.php | 12 +++ resources/views/suggestions/create.blade.php | 34 +++++++ resources/views/suggestions/index.blade.php | 48 +++++++++ resources/views/suggestions/show.blade.php | 47 +++++++++ routes/web.php | 22 +++++ 26 files changed, 851 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/Admin/UserController.php create mode 100644 app/Http/Controllers/Auth/LoginController.php create mode 100644 app/Http/Controllers/Auth/RegisterController.php create mode 100644 app/Http/Controllers/SuggestionController.php create mode 100644 app/Http/Middleware/CheckRole.php create mode 100644 app/Models/Suggestion.php create mode 100644 database/migrations/2025_08_27_130300_add_role_to_users_table.php create mode 100644 database/migrations/2025_08_27_130700_create_suggestions_table.php create mode 100644 resources/views/admin/users/create.blade.php create mode 100644 resources/views/admin/users/edit.blade.php create mode 100644 resources/views/admin/users/index.blade.php create mode 100644 resources/views/auth/login.blade.php create mode 100644 resources/views/auth/register.blade.php create mode 100644 resources/views/suggestions/create.blade.php create mode 100644 resources/views/suggestions/index.blade.php create mode 100644 resources/views/suggestions/show.blade.php diff --git a/AGENTS.md b/AGENTS.md index 6af42b9..30e1769 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php new file mode 100644 index 0000000..8adf719 --- /dev/null +++ b/app/Http/Controllers/Admin/UserController.php @@ -0,0 +1,77 @@ +middleware(['auth', 'role:' . User::ROLE_ADMIN]); + } + + 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.'); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..a9d146d --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,44 @@ +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('/'); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php new file mode 100644 index 0000000..87ab962 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -0,0 +1,36 @@ +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'); + } +} diff --git a/app/Http/Controllers/SuggestionController.php b/app/Http/Controllers/SuggestionController.php new file mode 100644 index 0000000..58837ed --- /dev/null +++ b/app/Http/Controllers/SuggestionController.php @@ -0,0 +1,99 @@ +middleware('auth'); + } + + 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.'); + } +} diff --git a/app/Http/Middleware/CheckRole.php b/app/Http/Middleware/CheckRole.php new file mode 100644 index 0000000..4d484fc --- /dev/null +++ b/app/Http/Middleware/CheckRole.php @@ -0,0 +1,35 @@ +role === $role) { + return $next($request); + } + } + + abort(403, 'Unauthorized action.'); + } +} diff --git a/app/Models/Suggestion.php b/app/Models/Suggestion.php new file mode 100644 index 0000000..2acd67e --- /dev/null +++ b/app/Models/Suggestion.php @@ -0,0 +1,36 @@ + 'array', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function model(): MorphTo + { + return $this->morphTo(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..37f401c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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. * @@ -21,6 +25,7 @@ class User extends Authenticatable 'name', 'email', 'password', + 'role', ]; /** diff --git a/bootstrap/app.php b/bootstrap/app.php index 6f869d9..a740399 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -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 { // diff --git a/compose.dev.yml b/compose.dev.yml index ce8ee75..2324407 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -1,6 +1,6 @@ services: web: - image: public.ecr.aws/nginx/nginx:1.25-alpine + image: nginx:1.25-alpine ports: - "80:80" volumes: @@ -47,7 +47,7 @@ services: - .env postgres: - image: public.ecr.aws/postgres/postgres:16-alpine + image: postgres:16-alpine ports: - "${POSTGRES_PORT:-5432}:5432" environment: @@ -60,7 +60,7 @@ services: - laravel-development redis: - image: public.ecr.aws/redis/redis:7-alpine + image: redis:7-alpine networks: - laravel-development diff --git a/database/migrations/2025_08_27_130300_add_role_to_users_table.php b/database/migrations/2025_08_27_130300_add_role_to_users_table.php new file mode 100644 index 0000000..6cc2ed9 --- /dev/null +++ b/database/migrations/2025_08_27_130300_add_role_to_users_table.php @@ -0,0 +1,28 @@ +string('role')->default('user')->after('email'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('role'); + }); + } +}; diff --git a/database/migrations/2025_08_27_130700_create_suggestions_table.php b/database/migrations/2025_08_27_130700_create_suggestions_table.php new file mode 100644 index 0000000..5103584 --- /dev/null +++ b/database/migrations/2025_08_27_130700_create_suggestions_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->string('type'); // create, update, delete + $table->string('model_type'); // e.g., App\Models\LexicalEntry + $table->unsignedBigInteger('model_id')->nullable(); + $table->json('data'); + $table->string('status')->default('pending'); // pending, approved, rejected + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('suggestions'); + } +}; diff --git a/docker/common/php-fpm/Dockerfile b/docker/common/php-fpm/Dockerfile index 4d6af90..fb3180d 100644 --- a/docker/common/php-fpm/Dockerfile +++ b/docker/common/php-fpm/Dockerfile @@ -1,15 +1,15 @@ # docker/common/php-fpm/Dockerfile -FROM public.ecr.aws/php/php:8.2-fpm-alpine AS production +FROM php:8.2-fpm-alpine AS production # Install system dependencies RUN apk add --no-cache \ build-base \ libpq-dev \ - libonig-dev \ - libssl-dev \ + oniguruma-dev \ + openssl-dev \ libxml2-dev \ - libcurl4-openssl-dev \ - libicu-dev \ + curl-dev \ + icu-dev \ libzip-dev \ libpng-dev \ libjpeg-turbo-dev @@ -25,7 +25,7 @@ RUN docker-php-ext-install \ gd # Install Composer -COPY --from=public.ecr.aws/composer/composer:2 /usr/bin/composer /usr/bin/composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer # Set working directory WORKDIR /var/www diff --git a/docker/development/workspace/Dockerfile b/docker/development/workspace/Dockerfile index 95436c9..7737354 100644 --- a/docker/development/workspace/Dockerfile +++ b/docker/development/workspace/Dockerfile @@ -1,5 +1,5 @@ # docker/development/workspace/Dockerfile -FROM public.ecr.aws/php/php:8.2-cli-alpine +FROM php:8.2-cli-alpine # Set environment variables for user and group ID ARG UID=1000 @@ -11,11 +11,11 @@ RUN apk add --no-cache \ curl \ unzip \ libpq-dev \ - libonig-dev \ - libssl-dev \ + oniguruma-dev \ + openssl-dev \ libxml2-dev \ - libcurl4-openssl-dev \ - libicu-dev \ + curl-dev \ + icu-dev \ libzip-dev \ nodejs \ npm @@ -31,7 +31,7 @@ RUN docker-php-ext-install \ soap # Install Composer -COPY --from=public.ecr.aws/composer/composer:2 /usr/bin/composer /usr/bin/composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer # Create a non-root user RUN addgroup -g ${GID} www && \ diff --git a/resources/views/admin/users/create.blade.php b/resources/views/admin/users/create.blade.php new file mode 100644 index 0000000..8fb1cd5 --- /dev/null +++ b/resources/views/admin/users/create.blade.php @@ -0,0 +1,56 @@ +@extends('layouts.app') + +@section('content') +
| Name | +Role | ++ Edit + | +|
|---|---|---|---|
| {{ $user->name }} | +{{ $user->email }} | +{{ $user->role }} | ++ Edit + + | +
Log in to suggest a change.
+ @endauth +| User | +Type | +Model | +Status | +Date | ++ View + | +
|---|---|---|---|---|---|
| {{ $suggestion->user->name }} | +{{ $suggestion->type }} | +{{ $suggestion->model_type }} | +{{ $suggestion->status }} | +{{ $suggestion->created_at->format('Y-m-d') }} | ++ View + | +
| No suggestions found. | +|||||
{{ $suggestion->user->name }} ({{ $suggestion->user->email }})
+{{ $suggestion->type }}
+{{ $suggestion->model_type }} @if($suggestion->model_id) (ID: {{ $suggestion->model_id }}) @endif
+{{ json_encode($suggestion->data, JSON_PRETTY_PRINT) }}
+ {{ $suggestion->status }}
+