init commit
This commit is contained in:
commit
c9d982669a
461 changed files with 30317 additions and 0 deletions
18
.editorconfig
Normal file
18
.editorconfig
Normal file
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
71
.env.example
Normal file
71
.env.example
Normal file
|
@ -0,0 +1,71 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
KEYCLOAK_CLIENT_ID=
|
||||
KEYCLOAK_CLIENT_SECRET=
|
||||
KEYCLOAK_REDIRECT_URI=/auth/callback
|
||||
KEYCLOAK_BASE_URL=
|
||||
KEYCLOAK_REALM=master
|
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
CHANGELOG.md export-ignore
|
||||
README.md export-ignore
|
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
/.phpunit.cache
|
||||
/bootstrap/ssr
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/manifest.json
|
||||
/public/service-worker.js
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/vendor
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/auth.json
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
.DS_Store
|
146
.oxlintrc.json
Normal file
146
.oxlintrc.json
Normal file
|
@ -0,0 +1,146 @@
|
|||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [
|
||||
"typescript",
|
||||
"unicorn"
|
||||
],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"node": true,
|
||||
"shared-node-browser": true
|
||||
},
|
||||
"rules": {
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-case-declarations": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-compare-neg-zero": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-constant-binary-expression": "error",
|
||||
"no-constant-condition": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-empty": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-empty-pattern": "error",
|
||||
"no-empty-static-block": "error",
|
||||
"no-ex-assign": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-func-assign": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-import-assign": "error",
|
||||
"no-invalid-regexp": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-new-native-nonconstructor": "error",
|
||||
"no-nonoctal-decimal-escape": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-self-assign": "error",
|
||||
"no-setter-return": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-unexpected-multiline": "off",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unsafe-negation": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-private-class-members": "error",
|
||||
"no-unused-vars": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-catch": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-with": "error",
|
||||
"require-yield": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"no-array-constructor": "error",
|
||||
"@typescript-eslint/no-duplicate-enum-values": "error",
|
||||
"@typescript-eslint/no-empty-object-type": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-extra-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/no-this-alias": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "error",
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "error",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"@typescript-eslint/no-wrapper-object-types": "error",
|
||||
"@typescript-eslint/prefer-as-const": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
"@typescript-eslint/triple-slash-reference": "error",
|
||||
"curly": "off",
|
||||
"unicorn/empty-brace-spaces": "off",
|
||||
"unicorn/no-nested-ternary": "off",
|
||||
"unicorn/number-literal-case": "off"
|
||||
},
|
||||
"globals": {
|
||||
"route": "readonly",
|
||||
"Laravel": "readonly"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"vendor",
|
||||
"node_modules",
|
||||
"public",
|
||||
"bootstrap/ssr",
|
||||
"tailwind.config.js",
|
||||
"resources/js/components/ui/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
"**/*.cts"
|
||||
],
|
||||
"rules": {
|
||||
"no-class-assign": "off",
|
||||
"no-const-assign": "off",
|
||||
"no-dupe-class-members": "off",
|
||||
"no-dupe-keys": "off",
|
||||
"no-func-assign": "off",
|
||||
"no-import-assign": "off",
|
||||
"no-new-native-nonconstructor": "off",
|
||||
"no-obj-calls": "off",
|
||||
"no-redeclare": "off",
|
||||
"no-setter-return": "off",
|
||||
"no-this-before-super": "off",
|
||||
"no-unsafe-negation": "off",
|
||||
"no-var": "error",
|
||||
"no-with": "off",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.svelte",
|
||||
"**/*.svelte"
|
||||
],
|
||||
"rules": {
|
||||
"no-inner-declarations": "off",
|
||||
"no-self-assign": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
resources/js/components/ui/*
|
||||
resources/views/mail/*
|
19
.prettierrc
Normal file
19
.prettierrc
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"singleAttributePerLine": false,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"printWidth": 150,
|
||||
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss", "prettier-plugin-svelte"],
|
||||
"tailwindFunctions": ["clsx", "cn"],
|
||||
"tabWidth": 4,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.yml",
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{ "files": "*.svelte", "options": { "parser": "svelte" } }
|
||||
]
|
||||
}
|
51
app/Console/Commands/MakeAdmin.php
Normal file
51
app/Console/Commands/MakeAdmin.php
Normal 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;
|
||||
}
|
||||
}
|
51
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
51
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal 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('/');
|
||||
}
|
||||
}
|
41
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
41
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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')]);
|
||||
}
|
||||
}
|
69
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
69
app/Http/Controllers/Auth/NewPasswordController.php
Normal 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)],
|
||||
]);
|
||||
}
|
||||
}
|
41
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
41
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal 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.'));
|
||||
}
|
||||
}
|
51
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
51
app/Http/Controllers/Auth/RegisteredUserController.php
Normal 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');
|
||||
}
|
||||
}
|
29
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
29
app/Http/Controllers/Auth/VerifyEmailController.php
Normal 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');
|
||||
}
|
||||
}
|
119
app/Http/Controllers/CompetitionController.php
Normal file
119
app/Http/Controllers/CompetitionController.php
Normal 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
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
40
app/Http/Controllers/Settings/PasswordController.php
Normal file
40
app/Http/Controllers/Settings/PasswordController.php
Normal 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();
|
||||
}
|
||||
}
|
22
app/Http/Controllers/Settings/PermissionsController.php
Normal file
22
app/Http/Controllers/Settings/PermissionsController.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
63
app/Http/Controllers/Settings/ProfileController.php
Normal file
63
app/Http/Controllers/Settings/ProfileController.php
Normal 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('/');
|
||||
}
|
||||
}
|
34
app/Http/Middleware/CheckPermission.php
Normal file
34
app/Http/Middleware/CheckPermission.php
Normal 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.');
|
||||
}
|
||||
}
|
33
app/Http/Middleware/CheckRole.php
Normal file
33
app/Http/Middleware/CheckRole.php
Normal 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.');
|
||||
}
|
||||
}
|
23
app/Http/Middleware/HandleAppearance.php
Normal file
23
app/Http/Middleware/HandleAppearance.php
Normal 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);
|
||||
}
|
||||
}
|
59
app/Http/Middleware/HandleInertiaRequests.php
Normal file
59
app/Http/Middleware/HandleInertiaRequests.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
85
app/Http/Requests/Auth/LoginRequest.php
Normal file
85
app/Http/Requests/Auth/LoginRequest.php
Normal 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());
|
||||
}
|
||||
}
|
30
app/Http/Requests/Settings/ProfileUpdateRequest.php
Normal file
30
app/Http/Requests/Settings/ProfileUpdateRequest.php
Normal 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),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
66
app/Models/BreakPeriod.php
Normal file
66
app/Models/BreakPeriod.php
Normal 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();
|
||||
}
|
||||
}
|
90
app/Models/Competition.php
Normal file
90
app/Models/Competition.php
Normal 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
64
app/Models/Field.php
Normal 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
105
app/Models/MatchGame.php
Normal 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
42
app/Models/Permission.php
Normal 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
47
app/Models/Role.php
Normal 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;
|
||||
}
|
||||
}
|
128
app/Models/SchedulingMode.php
Normal file
128
app/Models/SchedulingMode.php
Normal 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
70
app/Models/Team.php
Normal 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
158
app/Models/User.php
Normal 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;
|
||||
}
|
||||
}
|
55
app/Notifications/ForgotPassword.php
Normal file
55
app/Notifications/ForgotPassword.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
69
app/Providers/AppServiceProvider.php
Normal file
69
app/Providers/AppServiceProvider.php
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
117
app/Services/PermissionService.php
Normal file
117
app/Services/PermissionService.php
Normal 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;
|
||||
}
|
||||
}
|
18
artisan
Executable file
18
artisan
Executable file
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__ . '/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
34
bootstrap/app.php
Normal file
34
bootstrap/app.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Middleware\CheckPermission;
|
||||
use App\Http\Middleware\CheckRole;
|
||||
use App\Http\Middleware\HandleAppearance;
|
||||
use App\Http\Middleware\HandleInertiaRequests;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__ . '/../routes/web.php',
|
||||
commands: __DIR__ . '/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
||||
|
||||
$middleware->web(append: [
|
||||
HandleAppearance::class,
|
||||
HandleInertiaRequests::class,
|
||||
AddLinkHeadersForPreloadedAssets::class,
|
||||
]);
|
||||
|
||||
$middleware->alias([
|
||||
'permission' => CheckPermission::class,
|
||||
'role' => CheckRole::class
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
})->create();
|
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
6
bootstrap/providers.php
Normal file
6
bootstrap/providers.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
\SocialiteProviders\Manager\ServiceProvider::class
|
||||
];
|
16
components.json
Normal file
16
components.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "resources/css/app.css",
|
||||
"baseColor": "neutral"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"hooks": "@/hooks",
|
||||
"lib": "@/lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
}
|
85
composer.json
Normal file
85
composer.json
Normal file
|
@ -0,0 +1,85 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "oseughu/svelte-starter-kit",
|
||||
"type": "project",
|
||||
"description": "A laravel starter kit built with svelte 5, inertia 2 and shadcn-svelte",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"inertiajs/inertia-laravel": "^2.0.2",
|
||||
"laravel/framework": "^12.16.0",
|
||||
"laravel/socialite": "^5.21",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"socialiteproviders/keycloak": "^5.3",
|
||||
"tightenco/ziggy": "^2.5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.24.1",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.22.1",
|
||||
"laravel/sail": "^1.43.1",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/collision": "^8.8.0",
|
||||
"phpunit/phpunit": "^11.5.21"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"pint": [
|
||||
"./vendor/bin/pint"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
8802
composer.lock
generated
Normal file
8802
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
126
config/app.php
Normal file
126
config/app.php
Normal file
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
115
config/auth.php
Normal file
115
config/auth.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
]
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the amount of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
108
config/cache.php
Normal file
108
config/cache.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'),
|
||||
|
||||
];
|
174
config/database.php
Normal file
174
config/database.php
Normal file
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
80
config/filesystems.php
Normal file
80
config/filesystems.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL') . '/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
56
config/inertia.php
Normal file
56
config/inertia.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Server Side Rendering
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configures if and how Inertia uses Server Side Rendering
|
||||
| to pre-render the initial visits made to your application's pages.
|
||||
|
|
||||
| You can specify a custom SSR bundle path, or omit it to let Inertia
|
||||
| try and automatically detect it for you.
|
||||
|
|
||||
| Do note that enabling these options will NOT automatically make SSR work,
|
||||
| as a separate rendering service needs to be available. To learn more,
|
||||
| please visit https://inertiajs.com/server-side-rendering
|
||||
|
|
||||
*/
|
||||
|
||||
'ssr' => [
|
||||
'enabled' => true,
|
||||
|
||||
'url' => 'http://127.0.0.1:13714',
|
||||
|
||||
'bundle' => base_path('bootstrap/ssr/ssr.js'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Testing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The values described here are used to locate Inertia components on the
|
||||
| filesystem. For instance, when using `assertInertia`, the assertion
|
||||
| attempts to locate the component as a file relative to any of the
|
||||
| paths AND with any of the extensions specified here.
|
||||
|
|
||||
*/
|
||||
|
||||
'testing' => [
|
||||
'ensure_pages_exist' => true,
|
||||
|
||||
'page_paths' => [resource_path('js/pages')],
|
||||
|
||||
'page_extensions' => [
|
||||
'js',
|
||||
'jsx',
|
||||
'svelte',
|
||||
'ts',
|
||||
'tsx',
|
||||
'vue',
|
||||
],
|
||||
],
|
||||
];
|
132
config/logging.php
Normal file
132
config/logging.php
Normal file
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
116
config/mail.php
Normal file
116
config/mail.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
112
config/queue.php
Normal file
112
config/queue.php
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
46
config/services.php
Normal file
46
config/services.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_KEY'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
'keycloak' => [
|
||||
'client_id' => env('KEYCLOAK_CLIENT_ID'),
|
||||
'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
|
||||
'redirect' => env('KEYCLOAK_REDIRECT_URI'),
|
||||
'base_url' => env('KEYCLOAK_BASE_URL'), // Specify your keycloak server URL here
|
||||
'realms' => env('KEYCLOAK_REALM') // Specify your keycloak realm
|
||||
],
|
||||
|
||||
];
|
217
config/session.php
Normal file
217
config/session.php
Normal file
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "apc",
|
||||
| "memcached", "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "apc", "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug(env('APP_NAME', 'laravel'), '_') . '_session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain and all subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.sqlite*
|
34
database/factories/UserFactory.php
Normal file
34
database/factories/UserFactory.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'avatar' => fake()->imageUrl(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
}
|
55
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
55
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?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('users', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary(); // Changed from id() to uuid('id')->primary()
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password')->nullable(); // Made nullable for OIDC users
|
||||
$table->uuid('oidc_id')->nullable();
|
||||
$table->text('avatar')->nullable(); // Fixed typo: test -> text
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->uuid('user_id')->nullable()->index(); // Changed from foreignId to uuid
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
|
||||
// Add foreign key constraint for UUID
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?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('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?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('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
30
database/migrations/2025_06_22_121613_create_roles_table.php
Normal file
30
database/migrations/2025_06_22_121613_create_roles_table.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?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('roles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('display_name')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('roles');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
<?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('permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('display_name')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('is_wildcard')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('permissions');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
<?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('user_permission', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('permission_id')->constrained()->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'permission_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_permission');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?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::table('users', function (Blueprint $table) {
|
||||
$table->string('password')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('password')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
<?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('permission_role', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('permission_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
|
||||
$table->boolean('granted')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('permission_role');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
<?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('permission_user', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('permission_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->boolean('granted')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('permission_user');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
<?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('role_user', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('user_id');
|
||||
$table->unsignedBigInteger('role_id');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
|
||||
|
||||
$table->unique(['user_id', 'role_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('role_user');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,111 @@
|
|||
<?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
|
||||
{
|
||||
// Create competitions table
|
||||
Schema::create('competitions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->date('start_date');
|
||||
$table->date('end_date');
|
||||
$table->string('status')->default('planned');
|
||||
$table->string('location')->nullable();
|
||||
$table->integer('max_teams')->default(0);
|
||||
$table->integer('current_scheduling_mode_id')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Create scheduling modes table
|
||||
Schema::create('scheduling_modes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Create fields table
|
||||
Schema::create('fields', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('location')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->string('status')->default('planned');
|
||||
$table->integer('capacity')->default(0);
|
||||
$table->string('surface_type')->default('grass');
|
||||
$table->boolean('indoor')->default(false);
|
||||
$table->json('dimensions')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Create teams table
|
||||
Schema::create('teams', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('country')->nullable();
|
||||
$table->string('city')->nullable();
|
||||
$table->string('address')->nullable();
|
||||
$table->string('phone')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Create team members table
|
||||
Schema::create('team_members', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('team_id')->constrained('teams')->onDelete('cascade');
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->string('role')->default('member');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Create matches table
|
||||
Schema::create('matches', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('competition_id')->constrained('competitions')->onDelete('cascade');
|
||||
$table->foreignId('field_id')->constrained('fields')->onDelete('cascade');
|
||||
$table->foreignId('team1_id')->constrained('teams')->onDelete('cascade');
|
||||
$table->foreignId('team2_id')->constrained('teams')->onDelete('cascade');
|
||||
$table->string('status')->default('scheduled');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Create break periods table
|
||||
Schema::create('break_periods', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('competition_id')->constrained('competitions')->onDelete('cascade');
|
||||
$table->foreignId('field_id')->constrained('fields')->onDelete('cascade');
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->dateTime('start_time');
|
||||
$table->dateTime('end_time');
|
||||
$table->string('type')->default('break');
|
||||
$table->string('status')->default('planned');
|
||||
$table->integer('round')->default(1);
|
||||
$table->integer('match_slot')->default(1);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('break_periods');
|
||||
Schema::dropIfExists('matches');
|
||||
Schema::dropIfExists('team_members');
|
||||
Schema::dropIfExists('teams');
|
||||
Schema::dropIfExists('fields');
|
||||
Schema::dropIfExists('scheduling_modes');
|
||||
Schema::dropIfExists('competitions');
|
||||
}
|
||||
};
|
18
database/seeders/DatabaseSeeder.php
Normal file
18
database/seeders/DatabaseSeeder.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->call(RoleSeeder::class);
|
||||
}
|
||||
}
|
28
database/seeders/RoleSeeder.php
Normal file
28
database/seeders/RoleSeeder.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class RoleSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$roles = [
|
||||
['name' => 'admin', 'display_name' => 'Administrator'],
|
||||
['name' => 'user', 'display_name' => 'Regular User'],
|
||||
];
|
||||
|
||||
foreach ($roles as $role) {
|
||||
Role::firstOrCreate(
|
||||
['name' => $role['name']],
|
||||
$role
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
56
eslint.config.js
Normal file
56
eslint.config.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import js from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
|
||||
export default ts.config(
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
route: 'readonly',
|
||||
Laravel: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
// See more details at: https://typescript-eslint.io/packages/parser/
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
extraFileExtensions: ['.svelte', '.svelte.ts', '.svelte.js'], // Add support for additional file extensions, such as .svelte
|
||||
parser: ts.parser,
|
||||
// Specify a parser for each language, if needed:
|
||||
// parser: {
|
||||
// ts: ts.parser,
|
||||
// js: espree, // Use espree for .js files (add: import espree from 'espree')
|
||||
// typescript: ts.parser
|
||||
// },
|
||||
|
||||
// We recommend importing and specifying svelte.config.js.
|
||||
// By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly.
|
||||
// While certain Svelte settings may be statically loaded from svelte.config.js even if you don’t specify it,
|
||||
// explicitly specifying it ensures better compatibility and functionality.
|
||||
// svelteConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'svelte/infinite-reactive-loop': 'error',
|
||||
'svelte/no-at-html-tags': 'error',
|
||||
'svelte/no-target-blank': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js', 'resources/js/components/ui/*'],
|
||||
},
|
||||
prettier,
|
||||
);
|
66
package.json
Normal file
66
package.json
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:ssr": "vite build && vite build --ssr",
|
||||
"dev": "vite",
|
||||
"format": "prettier --write resources/",
|
||||
"format:check": "prettier --check resources/",
|
||||
"lint": "oxlint . --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@inertiajs/svelte": "^2.0.11",
|
||||
"@internationalized/date": "^3.8.1",
|
||||
"@lucide/svelte": "^0.511.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/table-core": "^8.21.3",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/node": "^22.15.27",
|
||||
"axios": "^1.9.0",
|
||||
"bits-ui": "^2.3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"embla-carousel-svelte": "^8.6.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.9.0",
|
||||
"formsnap": "^2.0.1",
|
||||
"globals": "^16.2.0",
|
||||
"laravel-vite-plugin": "^1.2.0",
|
||||
"layerchart": "^2.0.0-next.10",
|
||||
"lucide-svelte": "^0.511.0",
|
||||
"maildev": "^2.2.1",
|
||||
"mode-watcher": "^1.0.7",
|
||||
"oxlint": "^1.2.0",
|
||||
"paneforge": "^1.0.0-next.5",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.33.10",
|
||||
"svelte-check": "^4.2.1",
|
||||
"svelte-portal": "^2.2.1",
|
||||
"svelte-sonner": "^1.0.3",
|
||||
"svelte-transition": "^0.0.17",
|
||||
"sveltekit-superforms": "^2.25.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.3.2",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.33.0",
|
||||
"vaul-svelte": "^1.0.0-next.7",
|
||||
"vite": "^6.3.5",
|
||||
"zod": "^3.25.42"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "4.41.1",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.1.8",
|
||||
"lightningcss-linux-x64-gnu": "^1.30.1"
|
||||
}
|
||||
}
|
33
phpunit.xml
Normal file
33
phpunit.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
9
pint.json
Normal file
9
pint.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"preset": "laravel",
|
||||
"rules": {
|
||||
"concat_space": false,
|
||||
"not_operator_with_successor_space": false,
|
||||
"unary_operator_spaces": false,
|
||||
"function_declaration": false
|
||||
}
|
||||
}
|
5326
pnpm-lock.yaml
generated
Normal file
5326
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
onlyBuiltDependencies:
|
||||
- '@tailwindcss/oxide'
|
||||
- esbuild
|
25
public/.htaccess
Normal file
25
public/.htaccess
Normal file
|
@ -0,0 +1,25 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
0
public/favicon.ico
Normal file
0
public/favicon.ico
Normal file
20
public/index.php
Normal file
20
public/index.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
7
public/logo.svg
Normal file
7
public/logo.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<svg width="1994" height="508" viewBox="0 0 1994 508" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M484.904 121.113C485.084 121.78 485.176 122.467 485.178 123.157V230.481C485.178 231.859 484.815 233.212 484.125 234.404C483.435 235.596 482.443 236.584 481.25 237.27L391.267 289.132V391.926C391.267 394.724 389.782 397.306 387.359 398.715L199.527 506.958C199.097 507.202 198.628 507.359 198.159 507.525C197.984 507.584 197.817 507.691 197.632 507.74C196.319 508.087 194.939 508.087 193.626 507.74C193.411 507.682 193.215 507.564 193.01 507.486C192.58 507.33 192.131 507.193 191.72 506.958L3.92801 398.715C2.73502 398.029 1.74377 397.04 1.05416 395.848C0.364547 394.656 0.000945257 393.304 0 391.926L0 69.9554C0 69.2511 0.0977117 68.5664 0.273593 67.9012C0.33222 67.6762 0.469016 67.4708 0.547186 67.2459C0.693753 66.835 0.83055 66.4144 1.04552 66.0329C1.19208 65.7786 1.40705 65.5732 1.58293 65.3384C1.80767 65.0254 2.01286 64.7026 2.27668 64.4288C2.50142 64.2038 2.79456 64.0375 3.04861 63.8419C3.33197 63.6071 3.58602 63.3528 3.90847 63.1669L97.8192 9.04537C99.0082 8.36047 100.356 8 101.728 8C103.099 8 104.447 8.36047 105.636 9.04537L199.537 63.1669H199.557C199.869 63.3626 200.133 63.6071 200.417 63.8321C200.671 64.0277 200.954 64.2038 201.179 64.419C201.452 64.7026 201.648 65.0254 201.882 65.3384C202.048 65.5732 202.273 65.7786 202.41 66.0329C202.635 66.4242 202.762 66.835 202.918 67.2459C202.996 67.4708 203.133 67.6762 203.192 67.911C203.371 68.5776 203.463 69.2649 203.465 69.9554V271.055L281.713 225.952V123.148C281.713 122.463 281.81 121.768 281.986 121.113C282.055 120.878 282.182 120.673 282.26 120.448C282.416 120.037 282.553 119.617 282.768 119.235C282.915 118.981 283.13 118.775 283.296 118.541C283.53 118.228 283.726 117.905 283.999 117.631C284.224 117.406 284.507 117.24 284.761 117.044C285.054 116.809 285.309 116.555 285.621 116.369L379.542 62.2475C380.73 61.5616 382.078 61.2006 383.45 61.2006C384.822 61.2006 386.17 61.5616 387.359 62.2475L481.26 116.369C481.592 116.565 481.846 116.809 482.139 117.034C482.383 117.23 482.667 117.406 482.891 117.621C483.165 117.905 483.36 118.228 483.595 118.541C483.771 118.775 483.986 118.981 484.123 119.235C484.347 119.617 484.474 120.037 484.631 120.448C484.719 120.673 484.846 120.878 484.904 121.113ZM469.524 225.952V136.705L436.664 155.642L391.267 181.808V271.055L469.534 225.952H469.524ZM375.623 387.397V298.091L330.969 323.621L203.455 396.475V486.622L375.623 387.397ZM15.6534 83.5029V387.397L187.802 486.612V396.485L97.8681 345.532L97.8388 345.513L97.7997 345.493C97.4968 345.317 97.2427 345.063 96.9594 344.848C96.7151 344.652 96.4317 344.495 96.2167 344.28L96.1972 344.251C95.9432 344.006 95.7673 343.703 95.5523 343.429C95.3569 343.165 95.1224 342.94 94.966 342.666L94.9563 342.637C94.7804 342.343 94.6729 341.991 94.5459 341.659C94.4188 341.365 94.2527 341.091 94.1746 340.778C94.0769 340.407 94.0573 340.006 94.0182 339.624C93.9791 339.331 93.901 339.037 93.901 338.744V128.606L48.5139 102.43L15.6534 83.5029ZM101.737 24.872L23.4997 69.9554L101.718 115.039L179.946 69.9456L101.718 24.872H101.737ZM142.425 306.23L187.812 280.074V83.5029L154.951 102.44L109.554 128.606V325.177L142.425 306.23ZM383.45 78.0741L305.222 123.157L383.45 168.241L461.668 123.148L383.45 78.0741ZM375.623 181.808L330.227 155.642L297.366 136.705V225.952L342.753 252.108L375.623 271.055V181.808ZM195.619 382.927L310.362 317.351L367.719 284.583L289.549 239.529L199.547 291.401L117.518 338.675L195.619 382.927Z" fill="#FF2D20"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M659.562 71V352.743H762.045V401.34H605V71H659.562ZM954.655 211.153V181.441H1006.36V401.34H954.643V371.603C947.694 382.616 937.804 391.272 924.984 397.558C912.189 403.845 899.296 406.988 886.33 406.988C869.564 406.988 854.22 403.918 840.31 397.791C826.768 391.926 814.574 383.333 804.486 372.548C794.514 361.84 786.632 349.354 781.249 335.738C775.657 321.625 772.835 306.563 772.94 291.378C772.94 275.969 775.71 261.26 781.249 247.251C786.597 233.551 794.481 220.987 804.486 210.22C814.575 199.433 826.768 190.836 840.31 184.964C854.22 178.825 869.564 175.756 886.33 175.756C899.296 175.756 912.189 178.911 924.996 185.21C937.804 191.496 947.694 200.152 954.655 211.153ZM949.9 318.279C953.083 309.675 954.694 300.566 954.655 291.39C954.655 281.936 953.062 272.973 949.9 264.477C946.929 256.304 942.424 248.777 936.627 242.303C930.826 235.936 923.808 230.8 915.988 227.201C907.912 223.431 898.978 221.541 889.173 221.541C879.368 221.541 870.508 223.431 862.603 227.201C854.698 230.982 847.896 236.016 842.209 242.303C836.426 248.746 831.992 256.286 829.169 264.477C826.133 273.117 824.607 282.218 824.659 291.378C824.659 300.82 826.154 309.795 829.169 318.267C831.988 326.463 836.423 334.008 842.209 340.453C847.948 346.783 854.879 351.915 862.603 355.555C870.508 359.337 879.368 361.215 889.173 361.215C898.978 361.215 907.924 359.337 915.988 355.567C923.81 351.971 930.828 346.835 936.627 340.465C942.426 333.988 946.932 326.457 949.9 318.279ZM1047.16 401.328V181.428H1187.6V232.038H1098.87V401.328H1047.16ZM1375 211.153V181.441H1426.71V401.34H1374.99V371.603C1368.03 382.616 1358.15 391.272 1345.33 397.558C1332.54 403.845 1319.64 406.988 1306.68 406.988C1289.9 406.988 1274.57 403.918 1260.66 397.791C1247.11 391.926 1234.92 383.333 1224.83 372.548C1214.86 361.84 1206.98 349.354 1201.6 335.738C1196 321.625 1193.18 306.563 1193.29 291.378C1193.29 275.969 1196.04 261.26 1201.6 247.251C1206.94 233.551 1214.83 220.987 1224.83 210.22C1234.92 199.433 1247.11 190.836 1260.66 184.964C1274.57 178.825 1289.9 175.756 1306.68 175.756C1319.64 175.756 1332.54 178.911 1345.34 185.21C1358.15 191.496 1368.03 200.152 1375 211.153ZM1370.25 318.279C1373.42 309.674 1375.03 300.566 1374.99 291.39C1374.99 281.936 1373.4 272.973 1370.25 264.477C1367.27 256.303 1362.76 248.776 1356.96 242.303C1351.16 235.936 1344.14 230.8 1336.32 227.201C1328.26 223.431 1319.32 221.541 1309.52 221.541C1299.71 221.541 1290.85 223.431 1282.95 227.201C1275.04 230.982 1268.24 236.016 1262.56 242.303C1256.77 248.745 1252.33 256.285 1249.5 264.477C1246.47 273.117 1244.94 282.218 1244.99 291.378C1244.99 300.82 1246.49 309.795 1249.5 318.267C1252.33 326.464 1256.76 334.009 1262.56 340.453C1268.29 346.783 1275.22 351.915 1282.95 355.555C1290.85 359.337 1299.71 361.215 1309.52 361.215C1319.32 361.215 1328.27 359.337 1336.32 355.567C1344.14 351.971 1351.16 346.835 1356.96 340.465C1362.76 333.989 1367.27 326.458 1370.25 318.279ZM1799.18 175.781C1869.6 175.781 1917.62 237.846 1908.6 310.495H1737.05C1737.05 329.464 1756.3 366.139 1802.03 366.139C1841.37 366.139 1867.72 331.724 1867.74 331.699L1902.67 358.588C1871.44 391.763 1845.87 407 1805.81 407C1734.24 407 1685.74 361.964 1685.74 291.39C1685.74 227.545 1735.87 175.781 1799.18 175.781ZM1737.18 272.273H1861.11C1860.73 268.037 1854.02 216.629 1798.78 216.629C1743.54 216.629 1737.59 268.037 1737.18 272.273ZM1942.28 401.328V71H1994V401.328H1942.28Z" fill="#FF2D20"/>
|
||||
<path d="M1694.3 51.744L1662.55 147H1642.56L1611 51.744H1629.42L1652.55 117.992L1675.68 51.744H1694.3ZM1789.79 139.16C1787.57 141.251 1784.89 143.015 1781.75 144.452C1779.01 145.889 1775.61 147.131 1771.56 148.176C1767.51 149.221 1762.74 149.744 1757.25 149.744C1751.77 149.744 1746.21 149.025 1740.59 147.588C1734.98 146.151 1729.81 143.537 1725.11 139.748C1720.41 135.959 1716.42 130.863 1713.15 124.46C1709.89 117.927 1707.8 109.564 1706.88 99.372V51.744H1724.52V99.372C1724.91 105.513 1726.29 111.001 1728.64 115.836C1729.68 117.927 1730.99 119.952 1732.56 121.912C1734.13 123.872 1736.02 125.636 1738.24 127.204C1740.59 128.641 1743.27 129.817 1746.28 130.732C1749.41 131.647 1753.07 132.104 1757.25 132.104C1761.3 132.104 1764.83 131.647 1767.84 130.732C1770.97 129.817 1773.65 128.641 1775.87 127.204C1778.23 125.636 1780.19 123.872 1781.75 121.912C1783.32 119.952 1784.63 117.927 1785.67 115.836C1788.03 111.001 1789.4 105.513 1789.79 99.372V51.744H1807.43V147H1789.79V139.16ZM1913.11 117.012C1913.11 121.585 1911.87 125.832 1909.39 129.752C1906.91 133.672 1903.51 137.135 1899.2 140.14C1895.02 143.145 1890.05 145.497 1884.3 147.196C1878.68 148.895 1872.61 149.744 1866.07 149.744C1859.15 149.744 1852.61 148.437 1846.47 145.824C1840.33 143.211 1834.97 139.617 1830.4 135.044C1825.83 130.471 1822.23 125.113 1819.62 118.972C1817.01 112.831 1815.7 106.297 1815.7 99.372C1815.7 92.4467 1817.01 85.9133 1819.62 79.772C1822.23 73.6307 1825.83 68.3387 1830.4 63.896C1834.97 59.3227 1840.33 55.7293 1846.47 53.116C1852.61 50.5027 1859.15 49.196 1866.07 49.196C1872.87 49.196 1879.53 50.5027 1886.06 53.116C1892.6 55.7293 1898.28 59.5187 1903.12 64.484C1907.95 69.4493 1911.54 75.656 1913.9 83.104C1916.38 90.4213 1916.97 98.784 1915.66 108.192H1834.52C1834.52 110.544 1835.37 113.092 1837.06 115.836C1838.76 118.58 1840.98 121.193 1843.73 123.676C1846.6 126.028 1849.94 128.053 1853.72 129.752C1857.64 131.32 1861.76 132.104 1866.07 132.104C1869.86 132.104 1873.39 131.712 1876.66 130.928C1880.05 130.144 1882.99 129.099 1885.48 127.792C1887.96 126.355 1889.92 124.721 1891.36 122.892C1892.79 121.063 1893.51 119.103 1893.51 117.012H1913.11ZM1897.43 90.552C1897.04 88.4613 1896 86.1093 1894.3 83.496C1892.6 80.752 1890.38 78.1387 1887.63 75.656C1884.89 73.1733 1881.69 71.0827 1878.03 69.384C1874.37 67.6853 1870.38 66.836 1866.07 66.836C1861.76 66.836 1857.71 67.6853 1853.92 69.384C1850.26 71.0827 1847.06 73.1733 1844.32 75.656C1841.57 78.1387 1839.35 80.752 1837.65 83.496C1835.95 86.1093 1834.91 88.4613 1834.52 90.552H1897.43Z" fill="#080808"/>
|
||||
<path d="M1599.41 182L1569.5 232.965L1539.59 182H1440L1569.5 402.688L1699 182H1599.41Z" fill="#41B883"/>
|
||||
<path d="M1599.41 182L1569.5 232.965L1539.59 182H1491.8L1569.5 314.41L1647.2 182H1599.41Z" fill="#34495E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 9.5 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow:
|
167
resources/css/app.css
Normal file
167
resources/css/app.css
Normal file
|
@ -0,0 +1,167 @@
|
|||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@source "../views";
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: Instrument Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
|
||||
--color-chart-1: hsl(var(--chart-1));
|
||||
--color-chart-2: hsl(var(--chart-2));
|
||||
--color-chart-3: hsl(var(--chart-3));
|
||||
--color-chart-4: hsl(var(--chart-4));
|
||||
--color-chart-5: hsl(var(--chart-5));
|
||||
|
||||
--color-sidebar: hsl(var(--sidebar-background));
|
||||
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
||||
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
||||
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
||||
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
||||
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
||||
--color-sidebar-border: hsl(var(--sidebar-border));
|
||||
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
body,
|
||||
html {
|
||||
--font-sans:
|
||||
'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 92.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 92.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 0 0% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 0 0% 94%;
|
||||
--sidebar-accent-foreground: 0 0% 30%;
|
||||
--sidebar-border: 0 0% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 6.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 0 0% 7%;
|
||||
--sidebar-foreground: 0 0% 95.9%;
|
||||
--sidebar-primary: 360, 100%, 100%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 0 0% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
18
resources/js/app.ts
Normal file
18
resources/js/app.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte';
|
||||
import { hydrate, mount } from 'svelte';
|
||||
import '../css/app.css';
|
||||
import './bootstrap';
|
||||
|
||||
createInertiaApp({
|
||||
resolve: (name: string) => {
|
||||
const pages = import.meta.glob<ResolvedComponent>('./pages/**/*.svelte', { eager: true });
|
||||
return pages[`./pages/${name}.svelte`];
|
||||
},
|
||||
setup({ el, App, props }) {
|
||||
if (el && el.dataset.serverRendered === 'true') {
|
||||
hydrate(App, { target: el, props });
|
||||
} else if (el) {
|
||||
mount(App, { target: el, props });
|
||||
}
|
||||
},
|
||||
});
|
4
resources/js/bootstrap.ts
Normal file
4
resources/js/bootstrap.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
24
resources/js/components/AppContent.svelte
Normal file
24
resources/js/components/AppContent.svelte
Normal file
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { SidebarInset } from '@/components/ui/sidebar';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
variant?: 'header' | 'sidebar';
|
||||
class?: string;
|
||||
children: Snippet;
|
||||
};
|
||||
|
||||
let { variant, class: className, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if variant === 'sidebar'}
|
||||
<SidebarInset class={className}>
|
||||
<div class="mx-auto w-full max-w-7xl">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
{:else}
|
||||
<main class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl {className}">
|
||||
{@render children?.()}
|
||||
</main>
|
||||
{/if}
|
199
resources/js/components/AppHeader.svelte
Normal file
199
resources/js/components/AppHeader.svelte
Normal file
|
@ -0,0 +1,199 @@
|
|||
<script lang="ts">
|
||||
import AppLogo from '@/components/AppLogo.svelte';
|
||||
import AppLogoIcon from '@/components/AppLogoIcon.svelte';
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.svelte';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarTrigger } from '@/components/ui/menubar';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import UserMenuContent from '@/components/UserMenuContent.svelte';
|
||||
import { getInitials } from '@/hooks/useInitials';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
import { Link, page } from '@inertiajs/svelte';
|
||||
import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-svelte';
|
||||
import { navigationMenuTriggerStyle } from './ui/navigation-menu/navigation-menu-trigger.svelte';
|
||||
|
||||
interface NavItem {
|
||||
title: string;
|
||||
href: string;
|
||||
icon?: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
let { breadcrumbs = [] }: Props = $props();
|
||||
|
||||
let user = $derived($page.props.auth.user);
|
||||
|
||||
const isCurrentRoute = $derived((url: string) => $page.url === url);
|
||||
|
||||
const activeItemStyles = $derived((url: string) => (isCurrentRoute(url) ? 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' : ''));
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: '/dashboard',
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
];
|
||||
|
||||
const rightNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Repository',
|
||||
href: 'https://github.com/oseughu/svelte-starter-kit',
|
||||
icon: Folder,
|
||||
},
|
||||
{
|
||||
title: 'Documentation',
|
||||
href: 'https://laravel.com/docs/starter-kits',
|
||||
icon: BookOpen,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="border-b border-sidebar-border/80">
|
||||
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
|
||||
<div class="lg:hidden">
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
|
||||
<Menu class="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" class="w-[300px] p-6">
|
||||
<SheetTitle class="sr-only">Navigation Menu</SheetTitle>
|
||||
<SheetHeader class="flex justify-start text-left">
|
||||
<AppLogoIcon class="size-6 fill-current text-black dark:text-white" />
|
||||
</SheetHeader>
|
||||
<div class="flex h-full flex-1 flex-col justify-between space-y-4 py-6">
|
||||
<nav class="-mx-3 space-y-1">
|
||||
{#each mainNavItems as item (item.title)}
|
||||
<a
|
||||
href={item.href}
|
||||
class="flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent {activeItemStyles(
|
||||
item.href,
|
||||
)}"
|
||||
>
|
||||
{#if item.icon}
|
||||
<item.icon class="h-5 w-5" />
|
||||
{/if}
|
||||
{item.title}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="flex flex-col space-y-4">
|
||||
{#each rightNavItems as item (item.title)}
|
||||
<Link href={item.href} class="flex items-center space-x-2 text-sm font-medium">
|
||||
{#if item.icon}
|
||||
<item.icon class="h-5 w-5" />
|
||||
{/if}
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<Link href={route('dashboard')} class="flex items-center gap-x-2">
|
||||
<AppLogo />
|
||||
</Link>
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden h-full lg:flex lg:flex-1">
|
||||
<Menubar class="ml-10 flex h-full items-center border-none bg-transparent">
|
||||
{#each mainNavItems as item, index (index)}
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger
|
||||
value={item.href}
|
||||
class="{navigationMenuTriggerStyle()} {activeItemStyles(
|
||||
item.href,
|
||||
)} relative flex h-full cursor-pointer items-center px-4 text-sm font-medium"
|
||||
>
|
||||
{#if item.icon}
|
||||
<item.icon class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
{item.title}
|
||||
{#if isCurrentRoute(item.href)}
|
||||
<div class="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"></div>
|
||||
{/if}
|
||||
</MenubarTrigger>
|
||||
<MenubarContent align="start">
|
||||
<MenubarItem>
|
||||
<Link href={item.href}>
|
||||
{item.title}
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
{/each}
|
||||
</Menubar>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center space-x-2">
|
||||
<div class="relative flex items-center space-x-1">
|
||||
<Button variant="ghost" size="icon" class="group h-9 w-9 cursor-pointer">
|
||||
<Search class="size-5 opacity-80 group-hover:opacity-100" />
|
||||
</Button>
|
||||
|
||||
<div class="hidden space-x-1 lg:flex">
|
||||
{#each rightNavItems as item (item.title)}
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="ghost" size="icon" class="group h-9 w-9 cursor-pointer">
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer">
|
||||
<span class="sr-only">{item.title}</span>
|
||||
<item.icon class="size-5 opacity-80 group-hover:opacity-100" />
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{item.title}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="relative size-10 w-auto rounded-full p-1 focus-within:ring-2 focus-within:ring-primary"
|
||||
>
|
||||
<Avatar class="size-8 overflow-hidden rounded-full">
|
||||
{#if user.avatar}
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
{:else}
|
||||
<AvatarFallback class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white">
|
||||
{getInitials(user.name || '')}
|
||||
</AvatarFallback>
|
||||
{/if}
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="w-56">
|
||||
<UserMenuContent {user} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if breadcrumbs.length > 1}
|
||||
<div class="flex w-full border-b border-sidebar-border/70">
|
||||
<div class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl">
|
||||
<Breadcrumbs {breadcrumbs} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
12
resources/js/components/AppLogo.svelte
Normal file
12
resources/js/components/AppLogo.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import AppLogoIcon from '@/components/AppLogoIcon.svelte';
|
||||
</script>
|
||||
|
||||
<div class="inline-flex items-center gap-3">
|
||||
<div class="flex aspect-square size-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">
|
||||
<AppLogoIcon class="size-5 fill-current text-white! dark:text-black!" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="truncate text-sm font-semibold">Laravel Starter Kit</span>
|
||||
</div>
|
||||
</div>
|
12
resources/js/components/AppLogoIcon.svelte
Normal file
12
resources/js/components/AppLogoIcon.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
let { ...attrs } = $props();
|
||||
</script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {...attrs}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
|
||||
/>
|
||||
</svg>
|
25
resources/js/components/AppShell.svelte
Normal file
25
resources/js/components/AppShell.svelte
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||
import { page } from '@inertiajs/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'header' | 'sidebar';
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { variant = 'sidebar', class: className, children }: Props = $props();
|
||||
|
||||
const isOpen = $derived($page.props.sidebarOpen as boolean);
|
||||
</script>
|
||||
|
||||
{#if variant === 'header'}
|
||||
<div class="flex min-h-screen w-full flex-col {className}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{:else}
|
||||
<SidebarProvider open={isOpen}>
|
||||
{@render children?.()}
|
||||
</SidebarProvider>
|
||||
{/if}
|
55
resources/js/components/AppSidebar.svelte
Normal file
55
resources/js/components/AppSidebar.svelte
Normal file
|
@ -0,0 +1,55 @@
|
|||
<script lang="ts">
|
||||
import NavFooter from '@/components/NavFooter.svelte';
|
||||
import NavMain from '@/components/NavMain.svelte';
|
||||
import NavUser from '@/components/NavUser.svelte';
|
||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||
import { type NavItem } from '@/types';
|
||||
import { Link } from '@inertiajs/svelte';
|
||||
import { BookOpen, Folder, LayoutGrid } from 'lucide-svelte';
|
||||
import AppLogo from './AppLogo.svelte';
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: '/dashboard',
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
];
|
||||
|
||||
const footerNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Repository',
|
||||
href: 'https://github.com/oseughu/svelte-starter-kit',
|
||||
icon: Folder,
|
||||
},
|
||||
{
|
||||
title: 'Admin Dashboard',
|
||||
href: '/admin',
|
||||
icon: LayoutGrid,
|
||||
requireAdmin: true,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Sidebar collapsible="icon" variant="inset">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg">
|
||||
<Link href={route('dashboard')}>
|
||||
<AppLogo />
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<NavMain items={mainNavItems} />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<NavFooter items={footerNavItems} class="mt-auto" />
|
||||
<NavUser />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
23
resources/js/components/AppSidebarHeader.svelte
Normal file
23
resources/js/components/AppSidebarHeader.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.svelte';
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
|
||||
interface Props {
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
let { breadcrumbs = [] }: Props = $props();
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="flex h-16 shrink-0 items-center gap-2 border-b border-sidebar-border/70 px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SidebarTrigger class="-ml-1" />
|
||||
|
||||
{#if breadcrumbs.length > 0}
|
||||
<Breadcrumbs {breadcrumbs} />
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
43
resources/js/components/AppearanceTabs.svelte
Normal file
43
resources/js/components/AppearanceTabs.svelte
Normal file
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
import type { Appearance } from '@/hooks/useAppearance.svelte';
|
||||
import { useAppearance } from '@/hooks/useAppearance.svelte';
|
||||
import { Monitor, Moon, Sun } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const appearanceManager = useAppearance();
|
||||
// Create a local reactive variable that tracks the hook's value
|
||||
let currentAppearance = $state(appearanceManager.appearance);
|
||||
|
||||
// Update the local state when the appearance changes
|
||||
function handleUpdateAppearance(value: Appearance) {
|
||||
appearanceManager.updateAppearance(value);
|
||||
// Immediately update local state to ensure UI reflects the change
|
||||
currentAppearance = value;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ value: 'light', Icon: Sun, label: 'Light' },
|
||||
{ value: 'dark', Icon: Moon, label: 'Dark' },
|
||||
{ value: 'system', Icon: Monitor, label: 'System' },
|
||||
] as const;
|
||||
|
||||
let { class: className }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800 {className}">
|
||||
{#each tabs as { value, Icon, label } (value)}
|
||||
<button
|
||||
onclick={() => handleUpdateAppearance(value)}
|
||||
class="flex items-center rounded-md px-3.5 py-1.5 transition-colors
|
||||
{currentAppearance === value
|
||||
? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
|
||||
: 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60'}"
|
||||
>
|
||||
<Icon class="-ml-1 h-4 w-4" />
|
||||
<span class="ml-1.5 text-sm">{label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
34
resources/js/components/Breadcrumbs.svelte
Normal file
34
resources/js/components/Breadcrumbs.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { Breadcrumb, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Item } from '@/components/ui/breadcrumb';
|
||||
import { Link } from '@inertiajs/svelte';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
title: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
let { breadcrumbs }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{#each breadcrumbs as item, index (index)}
|
||||
<Item>
|
||||
{#if index === breadcrumbs.length - 1}
|
||||
<BreadcrumbPage>{item.title}</BreadcrumbPage>
|
||||
{:else}
|
||||
<BreadcrumbLink>
|
||||
<Link href={item.href ?? '#'}>{item.title}</Link>
|
||||
</BreadcrumbLink>
|
||||
{/if}
|
||||
</Item>
|
||||
{#if index !== breadcrumbs.length - 1}
|
||||
<BreadcrumbSeparator />
|
||||
{/if}
|
||||
{/each}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
82
resources/js/components/DeleteUser.svelte
Normal file
82
resources/js/components/DeleteUser.svelte
Normal file
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts">
|
||||
import HeadingSmall from '@/components/HeadingSmall.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useForm } from '@inertiajs/svelte';
|
||||
|
||||
let passwordInput = $state(null as unknown as HTMLInputElement);
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const deleteUser = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
$form.delete(route('profile.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeModal(),
|
||||
onError: () => passwordInput?.focus(),
|
||||
onFinish: () => $form.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
$form.clearErrors();
|
||||
$form.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<HeadingSmall title="Delete account" description="Delete your account and all of its resources" />
|
||||
<div class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
|
||||
<div class="relative space-y-0.5 text-red-600 dark:text-red-100">
|
||||
<p class="font-medium">Warning</p>
|
||||
<p class="text-sm">Please proceed with caution, this cannot be undone.</p>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button variant="destructive">Delete account</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<form class="space-y-6" onsubmit={deleteUser}>
|
||||
<DialogHeader class="space-y-3">
|
||||
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password
|
||||
to confirm you would like to permanently delete your account.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password" class="sr-only">Password</Label>
|
||||
<Input id="password" type="password" name="password" ref={passwordInput} bind:value={$form.password} placeholder="Password" />
|
||||
<InputError message={$form.errors.password} />
|
||||
</div>
|
||||
|
||||
<DialogFooter class="gap-2">
|
||||
<DialogClose>
|
||||
<Button variant="secondary" onclick={closeModal}>Cancel</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button variant="destructive" disabled={$form.processing}>
|
||||
<button type="submit">Delete account</button>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
15
resources/js/components/Heading.svelte
Normal file
15
resources/js/components/Heading.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
let { title, description }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="mb-8 space-y-0.5">
|
||||
<h2 class="text-xl font-semibold tracking-tight">{title}</h2>
|
||||
{#if description}
|
||||
<p class="text-sm text-muted-foreground">{description}</p>
|
||||
{/if}
|
||||
</div>
|
15
resources/js/components/HeadingSmall.svelte
Normal file
15
resources/js/components/HeadingSmall.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
let { title, description }: Props = $props();
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h3 class="mb-0.5 text-base font-medium">{title}</h3>
|
||||
{#if description}
|
||||
<p class="text-sm text-muted-foreground">{description}</p>
|
||||
{/if}
|
||||
</header>
|
22
resources/js/components/Icon.svelte
Normal file
22
resources/js/components/Icon.svelte
Normal file
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import * as icons from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
class?: string;
|
||||
size?: number | string;
|
||||
color?: string;
|
||||
strokeWidth?: number | string;
|
||||
}
|
||||
|
||||
let { name, class: className, size = 16, color, strokeWidth = 2 }: Props = $props();
|
||||
|
||||
const Component = $derived(() => {
|
||||
const iconName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return (icons as Record<string, any>)[iconName];
|
||||
});
|
||||
|
||||
const styles = $derived(['h-4 w-4', className]);
|
||||
</script>
|
||||
|
||||
<Component class={styles} {size} {color} {strokeWidth} />
|
14
resources/js/components/InputError.svelte
Normal file
14
resources/js/components/InputError.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
message?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { message, class: className }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if message}
|
||||
<p class="text-sm text-red-600 dark:text-red-500 {className}">
|
||||
{message}
|
||||
</p>
|
||||
{/if}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue