init commit

This commit is contained in:
unurled 2025-06-23 23:12:40 +02:00
commit c9d982669a
461 changed files with 30317 additions and 0 deletions

View file

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class MakeAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:make-admin {email : The email address of the user to make admin}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Makes someone an admin based on their email address';
/**
* Execute the console command.
*/
public function handle()
{
$email = $this->argument('email');
// Find the user by email
$user = User::where('email', $email)->first();
if (!$user) {
$this->error("User with email {$email} not found.");
return 1;
}
// Check if user is already an admin
if ($user->hasRole('admin')) {
$this->info("User {$email} is already an admin.");
return 0;
}
// Assign admin role
$user->assignRole('admin');
$this->info("User {$email} has been made an admin successfully.");
return 0;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
class AuthenticatedSessionController extends Controller
{
/**
* Show the login page.
*/
public function create(Request $request): Response
{
return Inertia::render('auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => $request->session()->get('status'),
]);
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password page.
*/
public function show(): Response
{
return Inertia::render('auth/ConfirmPassword');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class EmailVerificationPromptController extends Controller
{
/**
* Show the email verification prompt page.
*/
public function __invoke(Request $request): RedirectResponse|Response
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: Inertia::render('auth/VerifyEmail', ['status' => $request->session()->get('status')]);
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class NewPasswordController extends Controller
{
/**
* Show the password reset page.
*/
public function create(Request $request): Response
{
return Inertia::render('auth/ResetPassword', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PasswordReset) {
return to_route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [__($status)],
]);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Inertia\Inertia;
use Inertia\Response;
class PasswordResetLinkController extends Controller
{
/**
* Show the password reset link request page.
*/
public function create(Request $request): Response
{
return Inertia::render('auth/ForgotPassword', [
'status' => $request->session()->get('status'),
]);
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
Password::sendResetLink(
$request->only('email')
);
return back()->with('status', __('A reset link will be sent if the account exists.'));
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
class RegisteredUserController extends Controller
{
/**
* Show the registration page.
*/
public function create(): Response
{
return Inertia::render('auth/Register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return to_route('dashboard');
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
/** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */
$user = $request->user();
event(new Verified($user));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers;
use App\Models\Competition;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class CompetitionController extends Controller
{
public static function getPublicCompetitions(int $skip = 0, int $take = 10): JsonResponse
{
if ($skip < 0) {
$skip = 0;
}
if ($take < 1 || $take > 100) {
$take = 10;
}
$competitions = Competition::where('status', 'public')
->orderBy('start_date', 'desc')
->skip($skip)
->take($take)
->get();
$total = Competition::where('status', 'public')->count();
return response()->json([
'data' => $competitions,
'meta' => [
'skip' => $skip,
'take' => $take,
'total' => $total,
'hasMore' => ($skip + $take) < $total
]
]);
}
/**
* Get all public competitions/tournaments with pagination.
*
* @param Request $request
* @return JsonResponse
*/
public function getPublicCompetitions(Request $request): JsonResponse
{
$request->validate([
'skip' => 'integer|min:0',
'take' => 'integer|min:1|max:100',
]);
$skip = $request->input('skip', 0);
$take = $request->input('take', 10);
$competitions = Competition::where('status', 'public')
->orderBy('start_date', 'desc')
->skip($skip)
->take($take)
->get();
$total = Competition::where('status', 'public')->count();
return response()->json([
'data' => $competitions,
'meta' => [
'skip' => $skip,
'take' => $take,
'total' => $total,
'hasMore' => ($skip + $take) < $total
]
]);
}
/**
* Get competitions/tournaments for the authenticated user with pagination.
*
* @param Request $request
* @return JsonResponse
*/
public function getUserCompetitions(Request $request): JsonResponse
{
$request->validate([
'skip' => 'integer|min:0',
'take' => 'integer|min:1|max:100',
]);
$skip = $request->input('skip', 0);
$take = $request->input('take', 10);
$user = $request->user();
if (!$user) {
return response()->json(['error' => 'Unauthenticated'], 401);
}
$query = Competition::whereHas('teams', function ($query) use ($user) {
$query->whereHas('users', function ($query) use ($user) {
$query->where('users.id', $user->id);
});
});
$competitions = $query->orderBy('start_date', 'desc')
->skip($skip)
->take($take)
->get();
$total = $query->count();
return response()->json([
'data' => $competitions,
'meta' => [
'skip' => $skip,
'take' => $take,
'total' => $total,
'hasMore' => ($skip + $take) < $total
]
]);
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Inertia\Inertia;
use Inertia\Response;
class PasswordController extends Controller
{
/**
* Show the user's password settings page.
*/
public function edit(Request $request): Response
{
return Inertia::render('settings/Password');
}
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Permission;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class PermissionsController extends Controller
{
public function show(User $user)
{
$permissions = $user->permissions()->all();
Log::info($permissions);
return Inertia::render('Settings/Permissions', [
'permissions' => $permissions
]);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Settings\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Show the user's profile settings page.
*/
public function edit(Request $request): Response
{
return Inertia::render('settings/Profile', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => $request->session()->get('status'),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return to_route('profile.edit');
}
/**
* Delete the user's profile.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CheckPermission
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, ...$permissions)
{
if (!Auth::check()) {
return redirect()->route('login');
}
$user = Auth::user();
// Check if user has any of the required permissions
foreach ($permissions as $permission) {
if ($user->hasPermission($permission)) {
return $next($request);
}
}
// If no permissions match, return 403
abort(403, 'Unauthorized action.');
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class CheckRole
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, ...$roles)
{
if (!Auth::check()) {
return redirect()->route('login');
}
$user = Auth::user();
foreach ($roles as $role) {
if ($user->hasRole($role)) {
return $next($request);
}
}
abort(403, 'Unauthorized action.');
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpFoundation\Response;
class HandleAppearance
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
View::share('appearance', $request->cookie('appearance') ?? 'system');
return $next($request);
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Inspiring;
use Illuminate\Http\Request;
use Inertia\Middleware;
use Tighten\Ziggy\Ziggy;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that's loaded on the first page visit.
*
* @see https://inertiajs.com/server-side-setup#root-template
*
* @var string
*/
protected $rootView = 'app';
/**
* Determines the current asset version.
*
* @see https://inertiajs.com/asset-versioning
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @see https://inertiajs.com/shared-data
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
$user = $request->user();
$isAdmin = $user && $user->hasRole('admin');
return [
...parent::share($request),
'name' => config('app.name'),
'quote' => ['message' => trim($message), 'author' => trim($author)],
'auth' => [
'user' => $user,
'isAdmin' => $isAdmin,
],
'ziggy' => [
...(new Ziggy)->toArray(),
'location' => $request->url(),
],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
];
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Settings;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}

View file

@ -0,0 +1,66 @@
<?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\BelongsToMany;
class BreakPeriod extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'competition_id',
'field_id',
'name',
'description',
'start_time',
'end_time',
'type',
'status',
'round',
'match_slot',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'start_time' => 'datetime',
'end_time' => 'datetime',
];
/**
* Get the competition that this break period belongs to.
*/
public function competition(): BelongsTo
{
return $this->belongsTo(Competition::class);
}
/**
* Get the field that this break period belongs to.
*/
public function field(): BelongsTo
{
return $this->belongsTo(Field::class);
}
/**
* Get the teams that are on break during this period.
*/
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class)
->withTimestamps();
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Competition extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'description',
'start_date',
'end_date',
'status',
'location',
'max_teams',
'current_scheduling_mode_id',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'start_date' => 'date',
'end_date' => 'date',
];
/**
* Get the teams participating in the competition.
*/
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class)
->withTimestamps()
->withPivot('status');
}
/**
* Get the scheduling modes available for this competition.
*/
public function schedulingModes(): HasMany
{
return $this->hasMany(SchedulingMode::class);
}
/**
* Get the current scheduling mode for this competition.
*/
public function currentSchedulingMode()
{
return $this->belongsTo(SchedulingMode::class, 'current_scheduling_mode_id');
}
/**
* Get the matches for this competition.
*/
public function matches(): HasMany
{
return $this->hasMany(MatchGame::class);
}
/**
* Get the break periods for this competition.
*/
public function breakPeriods(): HasMany
{
return $this->hasMany(BreakPeriod::class);
}
/**
* Get the fields used in this competition.
*/
public function fields(): BelongsToMany
{
return $this->belongsToMany(Field::class)
->withTimestamps();
}
}

64
app/Models/Field.php Normal file
View file

@ -0,0 +1,64 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Field extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'location',
'description',
'status',
'capacity',
'surface_type',
'indoor',
'dimensions',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'indoor' => 'boolean',
'dimensions' => 'array',
];
/**
* Get the competitions that use this field.
*/
public function competitions(): BelongsToMany
{
return $this->belongsToMany(Competition::class)
->withTimestamps();
}
/**
* Get the matches scheduled on this field.
*/
public function matches(): HasMany
{
return $this->hasMany(MatchGame::class);
}
/**
* Get the break periods scheduled on this field.
*/
public function breakPeriods(): HasMany
{
return $this->hasMany(BreakPeriod::class);
}
}

105
app/Models/MatchGame.php Normal file
View file

@ -0,0 +1,105 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MatchGame extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'matches';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'competition_id',
'scheduling_mode_id',
'home_team_id',
'away_team_id',
'field_id',
'start_time',
'end_time',
'home_team_score',
'away_team_score',
'status',
'round',
'group',
'match_number',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'start_time' => 'datetime',
'end_time' => 'datetime',
'home_team_score' => 'integer',
'away_team_score' => 'integer',
];
/**
* Get the competition that the match belongs to.
*/
public function competition(): BelongsTo
{
return $this->belongsTo(Competition::class);
}
/**
* Get the scheduling mode that this match is part of.
*/
public function schedulingMode(): BelongsTo
{
return $this->belongsTo(SchedulingMode::class);
}
/**
* Get the home team.
*/
public function homeTeam(): BelongsTo
{
return $this->belongsTo(Team::class, 'home_team_id');
}
/**
* Get the away team.
*/
public function awayTeam(): BelongsTo
{
return $this->belongsTo(Team::class, 'away_team_id');
}
/**
* Get the field where the match is played.
*/
public function field(): BelongsTo
{
return $this->belongsTo(Field::class);
}
/**
* Determine if this match is during a break period.
*
* @return bool
*/
public function hasBankPeriod()
{
return BreakPeriod::where('competition_id', $this->competition_id)
->where('start_time', '<=', $this->start_time)
->where('end_time', '>=', $this->end_time)
->exists();
}
}

42
app/Models/Permission.php Normal file
View file

@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Permission extends Model
{
use HasFactory;
protected $fillable = [
'name',
'display_name',
'description',
'is_wildcard'
];
protected $casts = [
'is_wildcard' => 'boolean'
];
public function roles()
{
return $this->belongsToMany(Role::class)->withpivot('granted')->withTimestamps();
}
public function users()
{
return $this->belongsToMany(User::class)->withPivot('granted')->withTimestamps();
}
public function matches($permission)
{
if (!$this->is_wildcard) {
return $this->name === $permission;
}
$pattern = str_replace('*', '.*', $this->name);
return preg_match('/^' . $pattern . '$/', $permission);
}
}

47
app/Models/Role.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
use HasFactory;
protected $fillable = [
'name',
'display_name',
'description'
];
public function permissions()
{
return $this->belongsToMany(Permission::class)->withPivot('granted')->withTimestamps();
}
public function users()
{
return $this->belongsToMany(User::class)->withTimestamps();
}
public function hasPermission($permission)
{
return $this->permissions->where('pivot.granted', true)->contains('name', $permission) ||
$this->hasWildcardPermission($permission);
}
private function hasWildcardPermission($permission)
{
$wildcardPermissions = $this->permissions->where('is_wildcard', true)->where('pivot.granted', true);
foreach ($wildcardPermissions as $wildcardPermission) {
$pattern = str_replace('*', '.*', $wildcardPermission->name);
if (preg_match('/^' . $pattern . '$/', $permission)) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,128 @@
<?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\HasMany;
class SchedulingMode extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'competition_id',
'name',
'description',
'algorithm',
'config',
'sequence_order',
'status',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'config' => 'array',
];
/**
* Get the competition that this scheduling mode belongs to.
*/
public function competition(): BelongsTo
{
return $this->belongsTo(Competition::class);
}
/**
* Get the matches associated with this scheduling mode.
*/
public function matches(): HasMany
{
return $this->hasMany(MatchGame::class);
}
/**
* Get competitions that are currently using this scheduling mode.
*/
public function activeCompetitions(): HasMany
{
return $this->hasMany(Competition::class, 'current_scheduling_mode_id');
}
/**
* Check if this is the current active scheduling mode for its competition.
*
* @return bool
*/
public function isActive(): bool
{
return $this->competition->current_scheduling_mode_id === $this->id;
}
/**
* Generate match schedule based on the algorithm defined.
*
* @return array
*/
public function generateSchedule(): array
{
// This would contain the logic to delegate to different scheduling algorithms
// based on the 'algorithm' field
switch ($this->algorithm) {
case 'round_robin':
return $this->generateRoundRobinSchedule();
case 'knockout':
return $this->generateKnockoutSchedule();
case 'group_stage':
return $this->generateGroupStageSchedule();
default:
return [];
}
}
/**
* Example implementation for round-robin scheduling.
*
* @return array
*/
private function generateRoundRobinSchedule(): array
{
// Implementation would go here
return [];
}
/**
* Example implementation for knockout scheduling.
*
* @return array
*/
private function generateKnockoutSchedule(): array
{
// Implementation would go here
return [];
}
/**
* Example implementation for group stage scheduling.
*
* @return array
*/
private function generateGroupStageSchedule(): array
{
// Implementation would go here
return [];
}
}

70
app/Models/Team.php Normal file
View file

@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Team extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'logo',
'nb_players',
'contact_email',
'contact_phone',
'status',
];
/**
* Get the competitions this team is participating in.
*/
public function competitions(): BelongsToMany
{
return $this->belongsToMany(Competition::class)
->withTimestamps()
->withPivot('status');
}
/**
* Get the matches where this team is the home team.
*/
public function homeMatches(): HasMany
{
return $this->hasMany(MatchGame::class, 'home_team_id');
}
/**
* Get the matches where this team is the away team.
*/
public function awayMatches(): HasMany
{
return $this->hasMany(MatchGame::class, 'away_team_id');
}
/**
* Get all matches for this team (both home and away).
*/
public function matches()
{
return $this->homeMatches->merge($this->awayMatches);
}
/**
* Get the break periods assigned to this team.
*/
public function breakPeriods(): BelongsToMany
{
return $this->belongsToMany(BreakPeriod::class)
->withTimestamps();
}
}

158
app/Models/User.php Normal file
View file

@ -0,0 +1,158 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasUuids;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'avatar',
'oidc_id',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function roles()
{
return $this->belongsToMany(Role::class)->withTimestamps();
}
public function permissions()
{
return $this->belongsToMany(Permission::class)->withPivot('granted')->withTimestamps();
}
public function hasRole($role)
{
if (is_string($role)) {
return $this->roles->contains('name', $role);
}
return $this->roles->contains($role);
}
public function hasAnyRole($roles)
{
if (is_array($roles)) {
return $this->roles->whereIn('name', $roles)->isNotEmpty();
}
return $this->hasRole($roles);
}
public function hasPermission($permission)
{
// Check direct permissions
if ($this->hasDirectPermission($permission)) {
return true;
}
// Check role-based permissions
return $this->hasRolePermission($permission);
}
private function hasDirectPermission($permission)
{
// Check exact match with granted
if ($this->permissions->where('pivot.granted', true)->contains('name', $permission)) {
return true;
}
// Check wildcard permissions with granted
$wildcardPermissions = $this->permissions->where('is_wildcard', true)->where('pivot.granted', true);
foreach ($wildcardPermissions as $wildcardPermission) {
if ($wildcardPermission->matches($permission)) {
return true;
}
}
return false;
}
private function hasRolePermission($permission)
{
foreach ($this->roles as $role) {
if ($role->hasPermission($permission)) {
return true;
}
}
return false;
}
public function assignRole($role)
{
if (is_string($role)) {
$role = Role::where('name', $role)->firstOrFail();
}
$this->roles()->syncWithoutDetaching([$role->id]);
return $this;
}
public function removeRole($role)
{
if (is_string($role)) {
$role = Role::where('name', $role)->firstOrFail();
}
$this->roles()->detach($role->id);
return $this;
}
public function givePermission($permission)
{
if (is_string($permission)) {
$permission = Permission::where('name', $permission)->firstOrFail();
}
$this->permissions()->syncWithoutDetaching([$permission->id => ['granted' => true]]);
return $this;
}
public function revokePermission($permission)
{
if (is_string($permission)) {
$permission = Permission::where('name', $permission)->firstOrFail();
}
$this->permissions()->updateExistingPivot($permission->id, ['granted' => false]);
return $this;
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ForgotPassword extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct(public string $passwordResetCode) {}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
// ->from('')
->subject('Reset your password')
->line('Use the code below to reset your password')
->line($this->passwordResetCode)
->line('If this is not you, please feel free to ignore this message')
->line('Thank you');
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'message' => "Use this code to reset your password: {$this->passwordResetCode}",
'title' => 'Reset your password',
];
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('keycloak', \SocialiteProviders\Keycloak\Provider::class);
});
Blade::if('permission', function ($permission) {
return Auth::check() && Auth::user()->hasPermission($permission);
});
Blade::if('role', function ($role) {
return Auth::check() && Auth::user()->hasRole($role);
});
Blade::if('anyrole', function (...$roles) {
return Auth::check() && Auth::user()->hasAnyRole($roles);
});
Blade::if('allpermissions', function (...$permissions) {
if (!Auth::check()) {
return false;
}
foreach ($permissions as $permission) {
if (!Auth::user()->hasPermission($permission)) {
return false;
}
}
return true;
});
Blade::if('anypermission', function (...$permissions) {
if (!Auth::check()) {
return false;
}
foreach ($permissions as $permission) {
if (Auth::user()->hasPermission($permission)) {
return true;
}
}
return false;
});
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace App\Services;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
class PermissionService
{
public function createPermission($name, $displayName = null, $description = null)
{
$isWildcard = str_contains($name, '*');
return Permission::create([
'name' => $name,
'display_name' => $displayName ?? $name,
'description' => $description,
'is_wildcard' => $isWildcard
]);
}
public function createRole($name, $displayName = null, $description = null)
{
return Role::create([
'name' => $name,
'display_name' => $displayName ?? $name,
'description' => $description
]);
}
public function assignPermissionToRole($permission, $role)
{
if (is_string($permission)) {
$permission = Permission::where('name', $permission)->firstOrFail();
}
if (is_string($role)) {
$role = Role::where('name', $role)->firstOrFail();
}
$role->permissions()->syncWithoutDetaching([$permission->id]);
return $role;
}
public function removePermissionFromRole($permission, $role)
{
if (is_string($permission)) {
$permission = Permission::where('name', $permission)->firstOrFail();
}
if (is_string($role)) {
$role = Role::where('name', $role)->firstOrFail();
}
$role->permissions()->detach($permission->id);
return $role;
}
public function getUserPermissions(User $user)
{
// Get direct permissions
$directPermissions = $user->permissions;
// Get role-based permissions
$rolePermissions = collect();
foreach ($user->roles as $role) {
$rolePermissions = $rolePermissions->merge($role->permissions);
}
// Merge and remove duplicates
return $directPermissions->merge($rolePermissions)->unique('id');
}
public function checkPermission(User $user, $permission)
{
return $user->hasPermission($permission);
}
public function getMatchingPermissions($permissionPattern)
{
$allPermissions = Permission::all();
$matchingPermissions = collect();
foreach ($allPermissions as $perm) {
if ($perm->is_wildcard && $perm->matches($permissionPattern)) {
$matchingPermissions->push($perm);
} elseif ($perm->name === $permissionPattern) {
$matchingPermissions->push($perm);
}
}
return $matchingPermissions;
}
public function syncRolePermissions(Role $role, array $permissions)
{
$permissionIds = [];
foreach ($permissions as $permission) {
if (is_string($permission)) {
$perm = Permission::where('name', $permission)->first();
if ($perm) {
$permissionIds[] = $perm->id;
}
} else {
$permissionIds[] = $permission->id;
}
}
$role->permissions()->sync($permissionIds);
return $role;
}
}