Fix Redis port collision

This commit is contained in:
Hamza-Ayed
2026-08-26 16:48:32 +03:00
parent d02c9a4092
commit 458c3c1225
12 changed files with 383 additions and 8 deletions
@@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function register(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|in:student,teacher,guardian',
]);
$user = clone new User();
$user->name = $request->name;
$user->email = $request->email;
$user->password = Hash::make($request->password);
$user->role = $request->role;
$user->save();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'user' => $user,
], 201);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['البيانات المدخلة غير صحيحة.'],
]);
}
// Revoke all previous tokens for security if needed
$user->tokens()->delete();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'user' => $user,
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'تم تسجيل الخروج بنجاح.'
]);
}
public function deleteAccount(Request $request)
{
$user = $request->user();
// Revoke all tokens
$user->tokens()->delete();
// Delete user (soft delete if configured, or hard delete)
$user->delete();
return response()->json([
'message' => 'تم حذف الحساب بنجاح.'
]);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Course;
use Illuminate\Http\Request;
class CourseController extends Controller
{
public function index(Request $request)
{
$courses = Course::with('subject')->get();
return response()->json(['courses' => $courses]);
}
public function show(Course $course)
{
$course->load('lessons.quizzes');
return response()->json(['course' => $course]);
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Lesson;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class TeacherContentController extends Controller
{
public function storeLesson(Request $request)
{
if ($request->user()->role !== 'teacher') {
throw ValidationException::withMessages(['role' => 'غير مصرح لك بإضافة محتوى.']);
}
$request->validate([
'course_id' => 'required|exists:courses,id',
'title' => 'required|string|max:255',
'content' => 'required|string',
'order' => 'integer',
]);
$lesson = new Lesson();
$lesson->course_id = $request->course_id;
$lesson->title = $request->title;
$lesson->content = $request->content;
$lesson->order = $request->order ?? 1;
$lesson->save();
return response()->json(['lesson' => $lesson], 201);
}
}
+2 -1
View File
@@ -11,13 +11,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
#[Fillable(['name', 'email', 'password', 'role', 'api_token', 'grade', 'track'])] #[Fillable(['name', 'email', 'password', 'role', 'api_token', 'grade', 'track'])]
#[Hidden(['password', 'remember_token', 'api_token'])] #[Hidden(['password', 'remember_token', 'api_token'])]
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<UserFactory> */ /** @use HasFactory<UserFactory> */
use HasFactory, Notifiable; use HasApiTokens, HasFactory, Notifiable;
/** /**
* Get the attributes that should be cast. * Get the attributes that should be cast.
+7 -1
View File
@@ -21,6 +21,12 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// \Illuminate\Support\Facades\RateLimiter::for('api', function (\Illuminate\Http\Request $request) {
return \Illuminate\Cache\RateLimiting\Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
\Illuminate\Support\Facades\RateLimiter::for('auth', function (\Illuminate\Http\Request $request) {
return \Illuminate\Cache\RateLimiting\Limit::perMinute(5)->by($request->ip());
});
} }
} }
+1
View File
@@ -8,6 +8,7 @@
"require": { "require": {
"php": "^8.4", "php": "^8.4",
"laravel/framework": "^13.17", "laravel/framework": "^13.17",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^3.0" "laravel/tinker": "^3.0"
}, },
"require-dev": { "require-dev": {
+65 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "15ca84f430f0353684380a50b2f0c67d", "content-hash": "ab081cf41e953752e41017abd7d1c7be",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -1340,6 +1340,69 @@
}, },
"time": "2026-08-20T12:55:36+00:00" "time": "2026-08-20T12:55:36+00:00"
}, },
{
"name": "laravel/sanctum",
"version": "v4.3.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fee27a573d1a013af3721d86153a65e0b11927e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fee27a573d1a013af3721d86153a65e0b11927e6",
"reference": "fee27a573d1a013af3721d86153a65e0b11927e6",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/contracts": "^11.0|^12.0|^13.0",
"illuminate/database": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"php": "^8.2",
"symfony/console": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2026-06-23T18:26:55+00:00"
},
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
"version": "v2.0.16", "version": "v2.0.16",
@@ -8696,7 +8759,7 @@
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.3" "php": "^8.4"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.9.0" "plugin-api-version": "2.9.0"
+87
View File
@@ -0,0 +1,87 @@
<?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort(),
// Sanctum::currentRequestHost(),
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => AuthenticateSession::class,
'encrypt_cookies' => EncryptCookies::class,
'validate_csrf_token' => ValidateCsrfToken::class,
],
];
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};
+24
View File
@@ -18,3 +18,27 @@ Route::prefix('v1')->middleware('api.token')->group(function (): void {
Route::get('/guardian/students/{student}/summary', [GuardianSummaryController::class, 'show']) Route::get('/guardian/students/{student}/summary', [GuardianSummaryController::class, 'show'])
->name('guardian.students.summary'); ->name('guardian.students.summary');
}); });
use App\Http\Controllers\Api\AuthController;
Route::prefix('auth')->middleware('throttle:auth')->group(function () {
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
});
Route::prefix('auth')->middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::delete('/account', [AuthController::class, 'deleteAccount']);
});
use App\Http\Controllers\Api\CourseController;
use App\Http\Controllers\Api\TeacherContentController;
Route::middleware('auth:sanctum')->group(function () {
// Student/General Browsing
Route::get('/courses', [CourseController::class, 'index']);
Route::get('/courses/{course}', [CourseController::class, 'show']);
// Teacher Uploads
Route::post('/teacher/lessons', [TeacherContentController::class, 'storeLesson']);
});
+19 -1
View File
@@ -12,6 +12,8 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
server: server:
image: nginx:1.27-alpine image: nginx:1.27-alpine
@@ -24,8 +26,21 @@ services:
depends_on: depends_on:
- app - app
redis:
image: redis:alpine
container_name: saqel_redis
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
db: db:
image: mysql:8.0 image: mysql:8.4
container_name: saqel_mysql container_name: saqel_mysql
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -52,7 +67,10 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
volumes: volumes:
mysql_data: mysql_data:
storage_data: storage_data:
redis_data:
+1 -1
View File
@@ -2,7 +2,7 @@ FROM php:8.4-fpm-alpine
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions pdo_mysql bcmath intl zip gd opcache pcntl RUN install-php-extensions pdo_mysql bcmath intl zip gd opcache pcntl redis
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer COPY --from=composer:2 /usr/bin/composer /usr/bin/composer