@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Concerns\PasswordValidationRules;
|
||||
use App\Concerns\ProfileValidationRules;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules, ProfileValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
...$this->profileRules(),
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => $input['password'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Concerns\PasswordValidationRules;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => $input['password'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use App\Services\Encryption\TenantEnvelopeEncryption;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use RuntimeException;
|
||||
|
||||
class TenantEncrypted implements CastsAttributes
|
||||
{
|
||||
public function get(Model $model, string $key, mixed $value, array $attributes): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
$tenantId = (int) ($attributes['tenant_id'] ?? 0);
|
||||
if ($tenantId < 1) {
|
||||
throw new RuntimeException('TENANT_CONTEXT_REQUIRED_FOR_DECRYPTION');
|
||||
}
|
||||
|
||||
return app(TenantEnvelopeEncryption::class)->decrypt($tenantId, $value);
|
||||
}
|
||||
|
||||
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
$tenantId = (int) ($attributes['tenant_id'] ?? $model->getAttribute('tenant_id'));
|
||||
if ($tenantId < 1) {
|
||||
throw new RuntimeException('TENANT_CONTEXT_REQUIRED_FOR_ENCRYPTION');
|
||||
}
|
||||
|
||||
return app(TenantEnvelopeEncryption::class)->encrypt($tenantId, $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, Password|ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate the current password.
|
||||
*
|
||||
* @return array<int, Password|ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
protected function currentPasswordRules(): array
|
||||
{
|
||||
return ['required', 'string', 'current_password'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
trait ProfileValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate user profiles.
|
||||
*
|
||||
* @return array<string, array<int, ValidationRule|array<mixed>|string>>
|
||||
*/
|
||||
protected function profileRules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->nameRules(),
|
||||
'email' => $this->emailRules($userId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate user names.
|
||||
*
|
||||
* @return array<int, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
protected function nameRules(): array
|
||||
{
|
||||
return ['required', 'string', 'max:255'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate user emails.
|
||||
*
|
||||
* @return array<int, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
protected function emailRules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
$userId === null
|
||||
? Rule::unique(User::class)
|
||||
: Rule::unique(User::class)->ignore($userId),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
#[Signature('app:create-platform-admin {--name=} {--email=}')]
|
||||
#[Description('Create the first platform-level super administrator')]
|
||||
class CreatePlatformAdmin extends Command
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
if (config('deployment.mode') === 'self_hosted') {
|
||||
$this->error('Platform Super Admin hanya dibuat pada RADIQ control plane atau managed cloud.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$name = $this->option('name') ?: $this->ask('Full name');
|
||||
$email = $this->option('email') ?: $this->ask('Email address');
|
||||
$password = $this->secret('Password (minimum 12 characters)');
|
||||
$confirmation = $this->secret('Confirm password');
|
||||
|
||||
$validator = Validator::make(
|
||||
compact('name', 'email', 'password') + ['password_confirmation' => $confirmation],
|
||||
[
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'password' => ['required', 'confirmed', Password::min(12)->mixedCase()->numbers()->symbols()],
|
||||
],
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->all() as $error) {
|
||||
$this->error($error);
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$admin = new User;
|
||||
$admin->forceFill([
|
||||
'tenant_id' => null,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'email_verified_at' => now(),
|
||||
'password' => $password,
|
||||
'is_platform_admin' => true,
|
||||
'is_active' => true,
|
||||
])->save();
|
||||
|
||||
$this->info('Platform Master Admin created successfully.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Services\TenantProvisioningService;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
#[Signature('app:install-tenant {--name=} {--slug=} {--admin-name=} {--admin-email=} {--domain=}')]
|
||||
#[Description('Provision the only Tenant Admin account for a self-hosted installation')]
|
||||
class InstallTenant extends Command
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(TenantProvisioningService $service): int
|
||||
{
|
||||
if (config('deployment.mode') !== 'self_hosted') {
|
||||
$this->error('Command ini hanya boleh digunakan saat DEPLOYMENT_MODE=self_hosted.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
if (Tenant::exists()) {
|
||||
$this->error('Instalasi self-hosted ini sudah memiliki tenant.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
$data = [
|
||||
'name' => $this->option('name') ?: $this->ask('Nama tenant/ISP'),
|
||||
'slug' => $this->option('slug') ?: $this->ask('Slug tenant'),
|
||||
'owner_name' => $this->option('admin-name') ?: $this->ask('Nama Tenant Admin'),
|
||||
'owner_email' => $this->option('admin-email') ?: $this->ask('Email Tenant Admin'),
|
||||
'owner_password' => $this->secret('Password Tenant Admin (minimum 12 karakter)'),
|
||||
'deployment_type' => 'self_hosted', 'deployment_domain' => $this->option('domain'),
|
||||
];
|
||||
$confirmation = $this->secret('Konfirmasi password Tenant Admin');
|
||||
$validator = Validator::make($data + ['owner_password_confirmation' => $confirmation], [
|
||||
'name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'alpha_dash', 'max:100'],
|
||||
'owner_name' => ['required', 'string', 'max:255'], 'owner_email' => ['required', 'email'],
|
||||
'owner_password' => ['required', 'confirmed', Password::min(12)->mixedCase()->numbers()->symbols()],
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->all() as $error) {
|
||||
$this->error($error);
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
$service->createWithOwner($data);
|
||||
$this->info('RADIQ NDM self-hosted berhasil dipasang. Hanya akun Tenant Admin yang dibuat.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum DeploymentMode: string
|
||||
{
|
||||
case ControlPlane = 'control_plane';
|
||||
case ManagedCloud = 'managed_cloud';
|
||||
case SelfHosted = 'self_hosted';
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum LicenseStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Active = 'active';
|
||||
case Grace = 'grace';
|
||||
case Expired = 'expired';
|
||||
case Suspended = 'suspended';
|
||||
case Revoked = 'revoked';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum SystemRole: string
|
||||
{
|
||||
case MasterAdmin = 'MASTER ADMIN';
|
||||
case TenantAdmin = 'TENANT ADMIN';
|
||||
case TenantUser = 'TENANT USER';
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Administration;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Administration\StoreTenantRequest;
|
||||
use App\Http\Requests\Administration\UpdateTenantRequest;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\TenantProvisioningService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class TenantController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
$this->authorize('viewAny', Tenant::class);
|
||||
|
||||
$tenants = Tenant::query()
|
||||
->withCount('users')
|
||||
->with('installations:id,tenant_id,deployment_type,domain,status,last_seen_at')
|
||||
->orderBy('name')
|
||||
->paginate(15)
|
||||
->withQueryString();
|
||||
|
||||
return Inertia::render('administration/tenants/index', ['tenants' => $tenants]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
$this->authorize('create', Tenant::class);
|
||||
|
||||
return Inertia::render('administration/tenants/create');
|
||||
}
|
||||
|
||||
public function store(StoreTenantRequest $request, TenantProvisioningService $service): RedirectResponse
|
||||
{
|
||||
$tenant = $service->createWithOwner($request->validated());
|
||||
|
||||
return to_route('administration.tenants.edit', $tenant)
|
||||
->with('success', 'Tenant dan Tenant Owner berhasil dibuat.');
|
||||
}
|
||||
|
||||
public function edit(Tenant $tenant): Response
|
||||
{
|
||||
$this->authorize('update', $tenant);
|
||||
|
||||
$users = User::query()
|
||||
->withoutGlobalScope('tenant')
|
||||
->where('tenant_id', $tenant->id)
|
||||
->orderBy('name')
|
||||
->get(['uuid', 'name', 'email', 'is_active', 'created_at']);
|
||||
|
||||
return Inertia::render('administration/tenants/edit', [
|
||||
'tenant' => $tenant,
|
||||
'users' => $users,
|
||||
'installation' => $tenant->installations()->first(['id', 'deployment_type', 'domain', 'status', 'last_seen_at']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateTenantRequest $request, Tenant $tenant): RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $tenant);
|
||||
$data = $request->validated();
|
||||
$tenant->update(collect($data)->only(['name', 'slug', 'is_active'])->all());
|
||||
if (isset($data['deployment_type'])) {
|
||||
$tenant->installations()->first()?->update([
|
||||
'deployment_type' => $data['deployment_type'],
|
||||
'domain' => $data['deployment_domain'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Tenant berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function destroy(Tenant $tenant): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $tenant);
|
||||
abort_if($tenant->users()->exists(), 422, 'Tenant yang masih memiliki user tidak dapat dihapus. Nonaktifkan tenant sebagai gantinya.');
|
||||
$tenant->delete();
|
||||
|
||||
return to_route('administration.tenants.index')->with('success', 'Tenant berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Administration;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Administration\ResetUserPasswordRequest;
|
||||
use App\Http\Requests\Administration\StoreUserRequest;
|
||||
use App\Http\Requests\Administration\UpdateUserRequest;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(Tenant $tenant): Response
|
||||
{
|
||||
abort_unless(request()->user()->can('user.view'), 403);
|
||||
|
||||
$users = User::query()
|
||||
->with('roles:id,name')
|
||||
->when(! request()->user()->is_platform_admin, fn ($query) => $query->role(SystemRole::TenantUser->value))
|
||||
->orderBy('name')
|
||||
->paginate(15)
|
||||
->withQueryString();
|
||||
|
||||
return Inertia::render('administration/users/index', compact('tenant', 'users'));
|
||||
}
|
||||
|
||||
public function create(Tenant $tenant): Response
|
||||
{
|
||||
abort_unless(request()->user()->can('user.create'), 403);
|
||||
|
||||
return Inertia::render('administration/users/create', ['tenant' => $tenant]);
|
||||
}
|
||||
|
||||
public function store(StoreUserRequest $request, Tenant $tenant): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$user = User::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => $data['password'],
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
]);
|
||||
$user->forceFill(['email_verified_at' => now()])->save();
|
||||
$user->assignRole(SystemRole::TenantUser->value);
|
||||
|
||||
return to_route('administration.users.index', $tenant)->with('success', 'User berhasil dibuat.');
|
||||
}
|
||||
|
||||
public function edit(Tenant $tenant, User $user): Response
|
||||
{
|
||||
$this->ensureManageableUser($tenant, $user);
|
||||
abort_unless(request()->user()->can('user.update'), 403);
|
||||
|
||||
return Inertia::render('administration/users/edit', [
|
||||
'tenant' => $tenant,
|
||||
'managedUser' => $user->load('roles:id,name'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateUserRequest $request, Tenant $tenant, User $user): RedirectResponse
|
||||
{
|
||||
$this->ensureManageableUser($tenant, $user);
|
||||
$data = $request->validated();
|
||||
$user->update(['name' => $data['name'], 'email' => $data['email']]);
|
||||
|
||||
return back()->with('success', 'User berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function resetPassword(ResetUserPasswordRequest $request, Tenant $tenant, User $user): RedirectResponse
|
||||
{
|
||||
$this->ensureManageableUser($tenant, $user);
|
||||
$user->update(['password' => $request->validated('password')]);
|
||||
$user->tokens()->delete();
|
||||
|
||||
return back()->with('success', 'Password user berhasil direset dan token API dicabut.');
|
||||
}
|
||||
|
||||
public function toggleActive(Tenant $tenant, User $user): RedirectResponse
|
||||
{
|
||||
$this->ensureManageableUser($tenant, $user);
|
||||
abort_unless(request()->user()->can('user.update'), 403);
|
||||
abort_if(request()->user()->is($user), 422, 'Anda tidak dapat menonaktifkan akun sendiri.');
|
||||
abort_if($user->is_active && $user->hasRole(SystemRole::TenantAdmin->value), 422, 'Akun Tenant Admin utama tidak dapat dinonaktifkan dari pengelolaan user tenant.');
|
||||
$user->update(['is_active' => ! $user->is_active]);
|
||||
$user->tokens()->delete();
|
||||
|
||||
return back()->with('success', $user->is_active ? 'User diaktifkan.' : 'User dinonaktifkan.');
|
||||
}
|
||||
|
||||
public function destroy(Tenant $tenant, User $user): RedirectResponse
|
||||
{
|
||||
$this->ensureManageableUser($tenant, $user);
|
||||
abort_unless(request()->user()->can('user.delete'), 403);
|
||||
abort_if(request()->user()->is($user), 422, 'Anda tidak dapat menghapus akun sendiri.');
|
||||
abort_if($user->hasRole(SystemRole::TenantAdmin->value), 422, 'Akun Tenant Admin utama tidak dapat dihapus dari pengelolaan user tenant.');
|
||||
$user->delete();
|
||||
|
||||
return to_route('administration.users.index', $tenant)->with('success', 'User berhasil dihapus.');
|
||||
}
|
||||
|
||||
private function ensureManageableUser(Tenant $tenant, User $user): void
|
||||
{
|
||||
abort_unless($user->tenant_id === $tenant->id && ! $user->is_platform_admin, 404);
|
||||
abort_if(
|
||||
! request()->user()->is_platform_admin && ! $user->hasRole(SystemRole::TenantUser->value),
|
||||
403,
|
||||
'Tenant Admin hanya dapat mengelola Tenant User.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreTenantRequest;
|
||||
use App\Http\Requests\UpdateTenantRequest;
|
||||
use App\Http\Resources\TenantResource;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
class TenantController extends Controller
|
||||
{
|
||||
public function index(): AnonymousResourceCollection
|
||||
{
|
||||
$this->authorize('viewAny', Tenant::class);
|
||||
$user = request()->user();
|
||||
$query = Tenant::query()->orderBy('name');
|
||||
|
||||
if (! ($user->is_platform_admin && $user->can('tenant.access-any'))) {
|
||||
$query->whereKey($user->tenant_id);
|
||||
}
|
||||
|
||||
return TenantResource::collection($query->paginate());
|
||||
}
|
||||
|
||||
public function store(StoreTenantRequest $request): JsonResponse
|
||||
{
|
||||
$tenant = Tenant::create($request->validated());
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Tenant created successfully', 'data' => new TenantResource($tenant)], 201);
|
||||
}
|
||||
|
||||
public function show(Tenant $tenant): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $tenant);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Tenant retrieved successfully', 'data' => new TenantResource($tenant)]);
|
||||
}
|
||||
|
||||
public function update(UpdateTenantRequest $request, Tenant $tenant): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $tenant);
|
||||
$tenant->update($request->validated());
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Tenant updated successfully', 'data' => new TenantResource($tenant)]);
|
||||
}
|
||||
|
||||
public function destroy(Tenant $tenant): JsonResponse
|
||||
{
|
||||
$this->authorize('delete', $tenant);
|
||||
$tenant->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Tenant deleted successfully', 'data' => null]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
use AuthorizesRequests, ValidatesRequests;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\LicenseStatus;
|
||||
use App\Enums\SystemRole;
|
||||
use App\Models\DeploymentInstallation;
|
||||
use App\Models\Device;
|
||||
use App\Models\License;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke(): Response
|
||||
{
|
||||
$user = request()->user();
|
||||
|
||||
if ($user->is_platform_admin) {
|
||||
return Inertia::render('dashboard', [
|
||||
'role' => SystemRole::MasterAdmin->value,
|
||||
'tenant' => null,
|
||||
'stats' => [
|
||||
'tenants' => Tenant::count(),
|
||||
'activeTenants' => Tenant::where('is_active', true)->count(),
|
||||
'users' => User::withoutGlobalScope('tenant')->where('is_platform_admin', false)->count(),
|
||||
'activeLicenses' => License::withoutGlobalScope('tenant')->where('status', LicenseStatus::Active->value)->count(),
|
||||
'installations' => DeploymentInstallation::withoutGlobalScope('tenant')->count(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$role = $user->hasRole(SystemRole::TenantAdmin->value)
|
||||
? SystemRole::TenantAdmin->value
|
||||
: SystemRole::TenantUser->value;
|
||||
|
||||
return Inertia::render('dashboard', [
|
||||
'role' => $role,
|
||||
'tenant' => $user->tenant?->only(['id', 'name', 'slug', 'is_active']),
|
||||
'stats' => [
|
||||
'users' => User::count(),
|
||||
'activeUsers' => User::where('is_active', true)->count(),
|
||||
'devices' => Device::count(),
|
||||
'canAddDevice' => $user->can('device.create'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Models\Device;
|
||||
use App\Models\TenantDevicePolicy;
|
||||
use App\Services\Devices\MikrotikActivationService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Throwable;
|
||||
|
||||
class DeviceActivationController extends Controller
|
||||
{
|
||||
public function __invoke(Device $device, MikrotikActivationService $service): RedirectResponse
|
||||
{
|
||||
$user = request()->user();
|
||||
abort_if($user->is_platform_admin || $device->tenant_id !== $user->tenant_id, 404);
|
||||
abort_unless($user->can('device.connect'), 403);
|
||||
if ($user->hasRole(SystemRole::TenantUser->value)) {
|
||||
$policy = TenantDevicePolicy::where('tenant_id', $user->tenant_id)->first();
|
||||
abort_if(! $policy?->tenant_user_can_update_own || $device->created_by !== $user->id, 403);
|
||||
}
|
||||
|
||||
$device->update(['activation_status' => 'processing', 'activation_message' => null]);
|
||||
try {
|
||||
$service->activate($device->load('vendor'));
|
||||
$device->update(['activation_status' => 'active', 'activated_at' => now(), 'activation_message' => 'Group dan Base User RADIQ berhasil disinkronkan.']);
|
||||
|
||||
return back()->with('success', 'Perangkat aktif dan akun management RADIQ berhasil dibuat.');
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$message = str($exception->getMessage())->before(':')->limit(80)->toString();
|
||||
$device->update(['activation_status' => 'failed', 'activation_message' => $message]);
|
||||
|
||||
return back()->withErrors(['activation' => 'Aktivasi gagal: '.$message]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Http\Requests\StoreDeviceRequest;
|
||||
use App\Http\Requests\UpdateDeviceRequest;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceModel;
|
||||
use App\Models\DeviceType;
|
||||
use App\Models\DeviceVendor;
|
||||
use App\Models\TenantDevicePolicy;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DeviceController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->ensureTenantAccount();
|
||||
abort_unless($request->user()->can('device.view'), 403);
|
||||
$devices = Device::query()->with(['vendor:id,name', 'type:id,name', 'model:id,name', 'creator:id,name'])
|
||||
->when($request->string('search')->isNotEmpty(), fn ($query) => $query->where(fn ($nested) => $nested
|
||||
->where('name', 'ilike', '%'.$request->string('search').'%')
|
||||
->orWhere('management_address', 'ilike', '%'.$request->string('search').'%')
|
||||
->orWhere('location', 'ilike', '%'.$request->string('search').'%')))
|
||||
->when($request->integer('vendor'), fn ($query, $vendor) => $query->where('device_vendor_id', $vendor))
|
||||
->when($request->string('status')->isNotEmpty(), fn ($query) => $query->where('status', $request->string('status')))
|
||||
->latest()->paginate(15)->withQueryString();
|
||||
|
||||
return Inertia::render('devices/index', ['devices' => $devices, 'vendors' => DeviceVendor::where('is_active', true)->get(['id', 'name']), 'filters' => $request->only('search', 'vendor', 'status'), 'policy' => $this->policy(), 'isTenantAdmin' => $request->user()->hasRole(SystemRole::TenantAdmin->value)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
$this->authorizeCreate();
|
||||
|
||||
return Inertia::render('devices/form', $this->catalog() + ['device' => null]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StoreDeviceRequest $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeCreate();
|
||||
$this->validateModelCombination($request);
|
||||
Device::create($request->validated() + ['tenant_id' => $request->user()->tenant_id, 'created_by' => $request->user()->id, 'updated_by' => $request->user()->id]);
|
||||
|
||||
return to_route('devices.index')->with('success', 'Perangkat berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Device $device): Response
|
||||
{
|
||||
$this->ensureOwnedDevice($device);
|
||||
abort_unless(request()->user()->can('device.view'), 403);
|
||||
|
||||
return Inertia::render('devices/show', [
|
||||
'device' => $device->load(['vendor:id,name,slug', 'type:id,name', 'model:id,name', 'creator:id,name']),
|
||||
'credentials' => request()->user()->can('device.credential.view')
|
||||
? $device->credentials()->get(['id', 'name', 'username', 'connection_type', 'privilege_type', 'is_master', 'is_active', 'last_verified_at'])
|
||||
: [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Device $device): Response
|
||||
{
|
||||
$this->ensureOwnedDevice($device);
|
||||
$this->authorizeUpdate($device);
|
||||
|
||||
return Inertia::render('devices/form', $this->catalog() + ['device' => $device]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdateDeviceRequest $request, Device $device): RedirectResponse
|
||||
{
|
||||
$this->ensureOwnedDevice($device);
|
||||
$this->authorizeUpdate($device);
|
||||
$this->validateModelCombination($request);
|
||||
$device->update($request->validated() + ['updated_by' => $request->user()->id]);
|
||||
|
||||
return to_route('devices.show', $device)->with('success', 'Perangkat berhasil diperbarui.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Device $device): RedirectResponse
|
||||
{
|
||||
$this->ensureOwnedDevice($device);
|
||||
abort_unless(request()->user()->can('device.delete'), 403);
|
||||
abort_if(request()->user()->hasRole(SystemRole::TenantUser->value) && (! $this->policy()->tenant_user_can_delete_own || $device->created_by !== request()->user()->id), 403);
|
||||
$device->delete();
|
||||
|
||||
return to_route('devices.index')->with('success', 'Perangkat berhasil dihapus.');
|
||||
}
|
||||
|
||||
private function catalog(): array
|
||||
{
|
||||
return ['vendors' => DeviceVendor::where('is_active', true)->get(['id', 'name']), 'types' => DeviceType::where('is_active', true)->get(['id', 'name']), 'models' => DeviceModel::where('is_active', true)->get(['id', 'name', 'device_vendor_id', 'device_type_id'])];
|
||||
}
|
||||
|
||||
private function policy(): TenantDevicePolicy
|
||||
{
|
||||
return TenantDevicePolicy::firstOrCreate(['tenant_id' => request()->user()->tenant_id]);
|
||||
}
|
||||
|
||||
private function ensureTenantAccount(): void
|
||||
{
|
||||
abort_if(request()->user()->is_platform_admin || ! request()->user()->tenant_id, 403);
|
||||
}
|
||||
|
||||
private function ensureOwnedDevice(Device $device): void
|
||||
{
|
||||
$this->ensureTenantAccount();
|
||||
abort_unless($device->tenant_id === request()->user()->tenant_id, 404);
|
||||
}
|
||||
|
||||
private function authorizeCreate(): void
|
||||
{
|
||||
$this->ensureTenantAccount();
|
||||
abort_unless(request()->user()->can('device.create'), 403);
|
||||
abort_if(request()->user()->hasRole(SystemRole::TenantUser->value) && ! $this->policy()->tenant_user_can_create, 403);
|
||||
}
|
||||
|
||||
private function authorizeUpdate(Device $device): void
|
||||
{
|
||||
abort_unless(request()->user()->can('device.update'), 403);
|
||||
abort_if(request()->user()->hasRole(SystemRole::TenantUser->value) && (! $this->policy()->tenant_user_can_update_own || $device->created_by !== request()->user()->id), 403);
|
||||
}
|
||||
|
||||
private function validateModelCombination(Request $request): void
|
||||
{
|
||||
if ($request->filled('device_model_id')) {
|
||||
abort_unless(DeviceModel::whereKey($request->integer('device_model_id'))->where('device_vendor_id', $request->integer('device_vendor_id'))->where('device_type_id', $request->integer('device_type_id'))->exists(), 422, 'Model tidak sesuai dengan vendor dan tipe perangkat.');
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreDeviceCredentialRequest;
|
||||
use App\Http\Requests\UpdateDeviceCredentialRequest;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceCredential;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class DeviceCredentialController extends Controller
|
||||
{
|
||||
public function store(StoreDeviceCredentialRequest $request, Device $device): RedirectResponse
|
||||
{
|
||||
$this->ensureOwned($device);
|
||||
$data = $request->validated();
|
||||
$data['is_master'] = $data['privilege_type'] === 'master' || ($data['is_master'] ?? false);
|
||||
abort_if(($data['is_master'] ?? false) && ! $request->user()->can('device.master_account.manage'), 403);
|
||||
DeviceCredential::create($data + ['tenant_id' => $request->user()->tenant_id, 'device_id' => $device->id, 'created_by' => $request->user()->id, 'updated_by' => $request->user()->id]);
|
||||
|
||||
return back()->with('success', 'Credential perangkat berhasil disimpan secara terenkripsi.');
|
||||
}
|
||||
|
||||
public function update(UpdateDeviceCredentialRequest $request, Device $device, DeviceCredential $credential): RedirectResponse
|
||||
{
|
||||
$this->ensureOwned($device, $credential);
|
||||
abort_if($credential->is_master && ! $request->user()->can('device.master_account.manage'), 403);
|
||||
$data = $request->validated();
|
||||
$data['is_master'] = $data['privilege_type'] === 'master' || ($data['is_master'] ?? false);
|
||||
if (blank($data['password'] ?? null)) {
|
||||
unset($data['password']);
|
||||
}
|
||||
$credential->update($data + ['updated_by' => $request->user()->id]);
|
||||
|
||||
return back()->with('success', 'Credential perangkat berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function destroy(Device $device, DeviceCredential $credential): RedirectResponse
|
||||
{
|
||||
$this->ensureOwned($device, $credential);
|
||||
abort_unless(request()->user()->can('device.credential.delete'), 403);
|
||||
abort_if($credential->is_master && ! request()->user()->can('device.master_account.manage'), 403);
|
||||
$credential->delete();
|
||||
|
||||
return back()->with('success', 'Credential perangkat berhasil dihapus.');
|
||||
}
|
||||
|
||||
private function ensureOwned(Device $device, ?DeviceCredential $credential = null): void
|
||||
{
|
||||
abort_if(request()->user()->is_platform_admin || $device->tenant_id !== request()->user()->tenant_id, 404);
|
||||
abort_if($credential && ($credential->tenant_id !== $device->tenant_id || $credential->device_id !== $device->id), 404);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\ProfileDeleteRequest;
|
||||
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();
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]);
|
||||
|
||||
return to_route('profile.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's profile.
|
||||
*/
|
||||
public function destroy(ProfileDeleteRequest $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\PasswordUpdateRequest;
|
||||
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
class SecurityController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's security settings page.
|
||||
*/
|
||||
public function edit(TwoFactorAuthenticationRequest $request): Response
|
||||
{
|
||||
$props = [
|
||||
'canManageTwoFactor' => Features::canManageTwoFactorAuthentication(),
|
||||
'canManagePasskeys' => Features::canManagePasskeys(),
|
||||
'passkeys' => Features::canManagePasskeys()
|
||||
? $request->user()
|
||||
->passkeys()
|
||||
->select(['id', 'name', 'credential', 'created_at', 'last_used_at'])
|
||||
->latest()
|
||||
->get()
|
||||
->map(fn ($passkey) => [
|
||||
'id' => $passkey->id,
|
||||
'name' => $passkey->name,
|
||||
'authenticator' => $passkey->authenticator,
|
||||
'created_at_diff' => $passkey->created_at->diffForHumans(),
|
||||
'last_used_at_diff' => $passkey->last_used_at?->diffForHumans(),
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
: [],
|
||||
'passwordRules' => Password::defaults()->toPasswordRulesString(),
|
||||
];
|
||||
|
||||
if (Features::canManageTwoFactorAuthentication()) {
|
||||
$request->ensureStateIsValid();
|
||||
|
||||
$props['twoFactorEnabled'] = $request->user()->hasEnabledTwoFactorAuthentication();
|
||||
$props['requiresConfirmation'] = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm');
|
||||
}
|
||||
|
||||
return Inertia::render('settings/security', $props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(PasswordUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->update([
|
||||
'password' => $request->password,
|
||||
]);
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => __('Password updated.')]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Models\TenantDevicePolicy;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TenantDevicePolicyController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->hasRole(SystemRole::TenantAdmin->value), 403);
|
||||
$data = $request->validate([
|
||||
'tenant_user_can_create' => ['required', 'boolean'],
|
||||
'tenant_user_can_update_own' => ['required', 'boolean'],
|
||||
'tenant_user_can_delete_own' => ['required', 'boolean'],
|
||||
]);
|
||||
TenantDevicePolicy::updateOrCreate(['tenant_id' => $request->user()->tenant_id], $data);
|
||||
|
||||
return back()->with('success', 'Aturan perangkat Tenant User berhasil diperbarui.');
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Models\TenantDeviceSetting;
|
||||
use App\Models\TenantEncryptionKey;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class TenantDeviceSettingController extends Controller
|
||||
{
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
abort_unless($request->user()->hasRole(SystemRole::TenantAdmin->value), 403);
|
||||
$setting = TenantDeviceSetting::where('tenant_id', $request->user()->tenant_id)->first();
|
||||
|
||||
$key = TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $request->user()->tenant_id)->where('is_active', true)->first();
|
||||
|
||||
return Inertia::render('devices/settings', [
|
||||
'setting' => $setting?->only(['base_username', 'use_tls', 'verify_tls', 'connection_timeout']),
|
||||
'hasPassword' => (bool) $setting,
|
||||
'encryptionKey' => $key?->only(['version', 'source', 'created_at']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->hasRole(SystemRole::TenantAdmin->value), 403);
|
||||
$setting = TenantDeviceSetting::where('tenant_id', $request->user()->tenant_id)->first();
|
||||
$data = $request->validate([
|
||||
'base_username' => ['required', 'regex:/^[A-Za-z0-9][A-Za-z0-9_.@#-]*[A-Za-z0-9]$/', 'max:64'],
|
||||
'base_password' => [$setting ? 'nullable' : 'required', 'confirmed', Password::min(12)->mixedCase()->numbers()->symbols()],
|
||||
'use_tls' => ['required', 'boolean'], 'verify_tls' => ['required', 'boolean'],
|
||||
'connection_timeout' => ['required', 'integer', 'between:3,30'],
|
||||
]);
|
||||
if (blank($data['base_password'] ?? null)) {
|
||||
unset($data['base_password']);
|
||||
}
|
||||
TenantDeviceSetting::updateOrCreate(['tenant_id' => $request->user()->tenant_id], $data);
|
||||
|
||||
return back()->with('success', 'Base User perangkat berhasil disimpan secara terenkripsi.');
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Services\Encryption\TenantEnvelopeEncryption;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class TenantEncryptionController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, TenantEnvelopeEncryption $encryption): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->hasRole(SystemRole::TenantAdmin->value), 403);
|
||||
$data = $request->validate([
|
||||
'mode' => ['required', 'in:generated,manual'],
|
||||
'manual_key' => ['nullable', 'required_if:mode,manual', 'string', 'min:32', 'max:4096', 'confirmed'],
|
||||
'current_password' => ['required', 'string'],
|
||||
]);
|
||||
abort_unless(Hash::check($data['current_password'], $request->user()->password), 422, 'Password akun Tenant Admin tidak valid.');
|
||||
$key = $encryption->rotate($request->user()->tenant_id, $data['mode'] === 'manual' ? $data['manual_key'] : null, $request->user()->id);
|
||||
|
||||
return back()->with('success', "Encryption key tenant berhasil dirotasi ke versi {$key->version}. Semua secret database telah dienkripsi ulang.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureActiveUser
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($request->user() && ! $request->user()->is_active) {
|
||||
Auth::guard('web')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login')->withErrors(['email' => 'Akun Anda sedang dinonaktifkan.']);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -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(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
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
|
||||
{
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'permissions' => fn () => $request->user()?->getAllPermissions()->pluck('name')->values() ?? [],
|
||||
],
|
||||
'flash' => [
|
||||
'success' => fn () => $request->session()->get('success'),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Support\TenantContext;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SelectAdministrationTenant
|
||||
{
|
||||
public function __construct(private readonly TenantContext $context) {}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$tenant = $request->route('tenant');
|
||||
abort_unless($tenant instanceof Tenant, 404);
|
||||
|
||||
$user = $request->user();
|
||||
abort_unless($user?->is_platform_admin || $user?->tenant_id === $tenant->id, 403);
|
||||
|
||||
$previousTenantId = $this->context->id();
|
||||
$this->context->set($tenant->id);
|
||||
setPermissionsTeamId($tenant->id);
|
||||
|
||||
try {
|
||||
return $next($request);
|
||||
} finally {
|
||||
$this->context->set($previousTenantId);
|
||||
setPermissionsTeamId($previousTenantId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Support\TenantContext;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SetTenantContext
|
||||
{
|
||||
public function __construct(private readonly TenantContext $context) {}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
$requestedTenantId = $request->header('X-Tenant-ID');
|
||||
|
||||
if ($user?->is_platform_admin && $requestedTenantId !== null) {
|
||||
abort_unless($user->can('tenant.access-any'), 403);
|
||||
$this->context->set((int) $requestedTenantId);
|
||||
} else {
|
||||
$this->context->set($user?->tenant_id);
|
||||
}
|
||||
|
||||
setPermissionsTeamId($this->context->id());
|
||||
|
||||
try {
|
||||
return $next($request);
|
||||
} finally {
|
||||
$this->context->clear();
|
||||
setPermissionsTeamId(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Administration;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class ResetUserPasswordRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('user.update') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'password' => ['required', 'confirmed', Password::min(12)->mixedCase()->numbers()->symbols()],
|
||||
];
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Administration;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class StoreTenantRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('tenant.create') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['required', 'alpha_dash', 'max:100', 'unique:tenants,slug'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
'owner_name' => ['required', 'string', 'max:255'],
|
||||
'owner_email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'owner_password' => ['required', 'confirmed', Password::min(12)->mixedCase()->numbers()->symbols()],
|
||||
'deployment_type' => ['sometimes', 'in:managed_cloud,self_hosted'],
|
||||
'deployment_domain' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Administration;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class StoreUserRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('user.create') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'password' => ['required', 'confirmed', Password::min(12)->mixedCase()->numbers()->symbols()],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Administration;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateTenantRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('tenant.update') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['required', 'alpha_dash', 'max:100', 'unique:tenants,slug,'.$this->route('tenant')->id],
|
||||
'is_active' => ['required', 'boolean'],
|
||||
'deployment_type' => ['sometimes', 'in:managed_cloud,self_hosted'],
|
||||
'deployment_domain' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Administration;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateUserRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('user.update') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($this->route('user')->id)],
|
||||
];
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Concerns\PasswordValidationRules;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PasswordUpdateRequest extends FormRequest
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'current_password' => $this->currentPasswordRules(),
|
||||
'password' => $this->passwordRules(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Concerns\PasswordValidationRules;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ProfileDeleteRequest extends FormRequest
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'password' => $this->currentPasswordRules(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Concerns\ProfileValidationRules;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
use ProfileValidationRules;
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return $this->profileRules($this->user()->id);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Laravel\Fortify\InteractsWithTwoFactorState;
|
||||
|
||||
class TwoFactorAuthenticationRequest extends FormRequest
|
||||
{
|
||||
use InteractsWithTwoFactorState;
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreDeviceCredentialRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('device.credential.create') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return self::deviceCredentialRules();
|
||||
}
|
||||
|
||||
public static function deviceCredentialRules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'], 'username' => ['required', 'string', 'max:255'],
|
||||
'password' => ['required', 'string', 'max:4096'],
|
||||
'connection_type' => ['required', 'in:routeros_api,ssh,telnet,snmp,vendor_api'],
|
||||
'privilege_type' => ['required', 'in:master,noc,technician,monitoring,custom'],
|
||||
'is_master' => ['sometimes', 'boolean'], 'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreDeviceRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('device.create') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return self::deviceRules();
|
||||
}
|
||||
|
||||
public static function deviceRules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'], 'hostname' => ['nullable', 'string', 'max:255'],
|
||||
'device_vendor_id' => ['required', 'exists:device_vendors,id'], 'device_type_id' => ['required', 'exists:device_types,id'],
|
||||
'device_model_id' => ['nullable', 'exists:device_models,id'], 'serial_number' => ['nullable', 'string', 'max:255'],
|
||||
'management_address' => ['required', 'string', 'max:255'],
|
||||
'connection_type' => ['required', 'in:routeros_api,ssh,telnet,snmp,vendor_api'],
|
||||
'management_port' => ['required', 'integer', 'between:1,65535'], 'location' => ['nullable', 'string', 'max:255'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'], 'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'firmware_version' => ['nullable', 'string', 'max:255'], 'software_version' => ['nullable', 'string', 'max:255'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'], 'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreTenantRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('tenant.create') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['required', 'alpha_dash', 'max:100', 'unique:tenants,slug'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateDeviceCredentialRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('device.credential.update') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = StoreDeviceCredentialRequest::deviceCredentialRules();
|
||||
$rules['password'] = ['nullable', 'string', 'max:4096'];
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateDeviceRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('device.update') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return StoreDeviceRequest::deviceRules();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateTenantRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('tenant.update') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'slug' => ['sometimes', 'required', 'alpha_dash', 'max:100', 'unique:tenants,slug,'.$this->route('tenant')->id],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class TenantResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $this->uuid,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Support\TenantContext;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
trait BelongsToTenant
|
||||
{
|
||||
protected static function bootBelongsToTenant(): void
|
||||
{
|
||||
static::addGlobalScope('tenant', function (Builder $builder): void {
|
||||
$tenantId = app(TenantContext::class)->id();
|
||||
|
||||
if ($tenantId !== null) {
|
||||
$builder->where($builder->qualifyColumn('tenant_id'), $tenantId);
|
||||
}
|
||||
});
|
||||
|
||||
static::creating(function (self $model): void {
|
||||
$tenantId = app(TenantContext::class)->id();
|
||||
|
||||
if ($tenantId !== null && $model->getAttribute('tenant_id') === null) {
|
||||
$model->setAttribute('tenant_id', $tenantId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\DeploymentMode;
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DeploymentInstallation extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = ['tenant_id', 'name', 'deployment_type', 'domain', 'instance_key_hash', 'fingerprint_hash', 'status', 'activated_at', 'last_seen_at'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(fn (self $installation) => $installation->uuid ??= (string) Str::uuid());
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['deployment_type' => DeploymentMode::class, 'activated_at' => 'immutable_datetime', 'last_seen_at' => 'immutable_datetime'];
|
||||
}
|
||||
|
||||
public function licenses(): HasMany
|
||||
{
|
||||
return $this->hasMany(License::class);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Device extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = ['tenant_id', 'name', 'hostname', 'device_vendor_id', 'device_type_id', 'device_model_id', 'serial_number', 'management_address', 'connection_type', 'management_port', 'location', 'latitude', 'longitude', 'firmware_version', 'software_version', 'status', 'last_seen_at', 'notes', 'is_active', 'created_by', 'updated_by', 'activation_status', 'activated_at', 'activation_message'];
|
||||
|
||||
protected $hidden = ['credentials'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(fn (Device $device) => $device->uuid ??= (string) Str::uuid());
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean', 'last_seen_at' => 'datetime', 'activated_at' => 'datetime', 'latitude' => 'decimal:7', 'longitude' => 'decimal:7'];
|
||||
}
|
||||
|
||||
public function vendor()
|
||||
{
|
||||
return $this->belongsTo(DeviceVendor::class, 'device_vendor_id');
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
return $this->belongsTo(DeviceType::class, 'device_type_id');
|
||||
}
|
||||
|
||||
public function model()
|
||||
{
|
||||
return $this->belongsTo(DeviceModel::class, 'device_model_id');
|
||||
}
|
||||
|
||||
public function credentials()
|
||||
{
|
||||
return $this->hasMany(DeviceCredential::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Casts\TenantEncrypted;
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DeviceCredential extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = ['tenant_id', 'device_id', 'name', 'username', 'password', 'connection_type', 'privilege_type', 'is_master', 'is_active', 'last_verified_at', 'created_by', 'updated_by'];
|
||||
|
||||
protected $hidden = ['password'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(fn (DeviceCredential $credential) => $credential->uuid ??= (string) Str::uuid());
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['password' => TenantEncrypted::class, 'is_master' => 'boolean', 'is_active' => 'boolean', 'last_verified_at' => 'datetime'];
|
||||
}
|
||||
|
||||
public function device()
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DeviceModel extends Model
|
||||
{
|
||||
protected $fillable = ['device_vendor_id', 'device_type_id', 'name', 'model_code', 'description', 'is_active'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean'];
|
||||
}
|
||||
|
||||
public function vendor()
|
||||
{
|
||||
return $this->belongsTo(DeviceVendor::class, 'device_vendor_id');
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
return $this->belongsTo(DeviceType::class, 'device_type_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DeviceType extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'slug', 'is_active'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean'];
|
||||
}
|
||||
|
||||
public function models()
|
||||
{
|
||||
return $this->hasMany(DeviceModel::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DeviceVendor extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'slug', 'description', 'is_active'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean'];
|
||||
}
|
||||
|
||||
public function models()
|
||||
{
|
||||
return $this->hasMany(DeviceModel::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\LicenseStatus;
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class License extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = ['tenant_id', 'deployment_installation_id', 'license_key_hash', 'plan', 'status', 'max_devices', 'max_users', 'features', 'signed_payload', 'starts_at', 'expires_at', 'grace_until', 'issued_by'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(fn (self $license) => $license->uuid ??= (string) Str::uuid());
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['status' => LicenseStatus::class, 'features' => 'array', 'starts_at' => 'immutable_datetime', 'expires_at' => 'immutable_datetime', 'grace_until' => 'immutable_datetime'];
|
||||
}
|
||||
|
||||
public function installation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DeploymentInstallation::class, 'deployment_installation_id');
|
||||
}
|
||||
|
||||
public function issuer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'issued_by');
|
||||
}
|
||||
|
||||
public function events(): HasMany
|
||||
{
|
||||
return $this->hasMany(LicenseEvent::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LicenseEvent extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = ['tenant_id', 'license_id', 'deployment_installation_id', 'actor_id', 'event_type', 'metadata', 'occurred_at'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(fn (self $event) => $event->uuid ??= (string) Str::uuid());
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['metadata' => 'array', 'occurred_at' => 'immutable_datetime'];
|
||||
}
|
||||
|
||||
public function license(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(License::class);
|
||||
}
|
||||
|
||||
public function installation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DeploymentInstallation::class, 'deployment_installation_id');
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class Tenant extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name', 'slug', 'is_active'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (Tenant $tenant): void {
|
||||
$tenant->uuid ??= (string) Str::uuid();
|
||||
});
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean'];
|
||||
}
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
public function roles(): HasMany
|
||||
{
|
||||
return $this->hasMany(Role::class);
|
||||
}
|
||||
|
||||
public function installations(): HasMany
|
||||
{
|
||||
return $this->hasMany(DeploymentInstallation::class);
|
||||
}
|
||||
|
||||
public function licenses(): HasMany
|
||||
{
|
||||
return $this->hasMany(License::class);
|
||||
}
|
||||
|
||||
public function devices(): HasMany
|
||||
{
|
||||
return $this->hasMany(Device::class);
|
||||
}
|
||||
|
||||
public function devicePolicy()
|
||||
{
|
||||
return $this->hasOne(TenantDevicePolicy::class);
|
||||
}
|
||||
|
||||
public function deviceSetting()
|
||||
{
|
||||
return $this->hasOne(TenantDeviceSetting::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantDevicePolicy extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = ['tenant_id', 'tenant_user_can_create', 'tenant_user_can_update_own', 'tenant_user_can_delete_own'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['tenant_user_can_create' => 'boolean', 'tenant_user_can_update_own' => 'boolean', 'tenant_user_can_delete_own' => 'boolean'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Casts\TenantEncrypted;
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantDeviceSetting extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = ['tenant_id', 'base_username', 'base_password', 'use_tls', 'verify_tls', 'connection_timeout'];
|
||||
|
||||
protected $hidden = ['base_password'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['base_password' => TenantEncrypted::class, 'use_tls' => 'boolean', 'verify_tls' => 'boolean', 'connection_timeout' => 'integer'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TenantEncryptionKey extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = ['tenant_id', 'version', 'wrapped_key', 'is_active', 'source', 'created_by', 'retired_at'];
|
||||
|
||||
protected $hidden = ['wrapped_key'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(fn (self $key) => $key->uuid ??= (string) Str::uuid());
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean', 'retired_at' => 'immutable_datetime'];
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\Contracts\PasskeyUser;
|
||||
use Laravel\Fortify\PasskeyAuthenticatable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property Carbon|null $email_verified_at
|
||||
* @property string $password
|
||||
* @property string|null $two_factor_secret
|
||||
* @property string|null $two_factor_recovery_codes
|
||||
* @property Carbon|null $two_factor_confirmed_at
|
||||
* @property string|null $remember_token
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*/
|
||||
#[Fillable(['tenant_id', 'name', 'email', 'password', 'is_active'])]
|
||||
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
||||
class User extends Authenticatable implements PasskeyUser
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use BelongsToTenant, HasApiTokens, HasFactory, HasRoles, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (User $user): void {
|
||||
$user->uuid ??= (string) Str::uuid();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'two_factor_confirmed_at' => 'datetime',
|
||||
'is_platform_admin' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Network\Clients\RouterOs;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class RouterOsApiClient
|
||||
{
|
||||
/** @var resource|null */
|
||||
private $socket;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $host,
|
||||
private readonly int $port,
|
||||
private readonly string $username,
|
||||
private readonly string $password,
|
||||
private readonly int $timeout = 10,
|
||||
private readonly bool $tls = false,
|
||||
private readonly bool $verifyTls = true,
|
||||
) {}
|
||||
|
||||
public function connect(): void
|
||||
{
|
||||
$transport = $this->tls ? 'tls' : 'tcp';
|
||||
$context = stream_context_create(['ssl' => ['verify_peer' => $this->verifyTls, 'verify_peer_name' => $this->verifyTls, 'SNI_enabled' => true]]);
|
||||
$socket = @stream_socket_client("{$transport}://{$this->host}:{$this->port}", $errorNumber, $errorMessage, $this->timeout, STREAM_CLIENT_CONNECT, $context);
|
||||
if (! is_resource($socket)) {
|
||||
throw new RuntimeException('DEVICE_UNREACHABLE: koneksi RouterOS API gagal.');
|
||||
}
|
||||
$this->socket = $socket;
|
||||
stream_set_timeout($this->socket, $this->timeout);
|
||||
$this->command(['/login', '=name='.$this->username, '=password='.$this->password]);
|
||||
}
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
if (is_resource($this->socket)) {
|
||||
fclose($this->socket);
|
||||
}
|
||||
$this->socket = null;
|
||||
}
|
||||
|
||||
/** @return list<array<string, string>> */
|
||||
public function command(array $words): array
|
||||
{
|
||||
if (! is_resource($this->socket)) {
|
||||
throw new RuntimeException('DRIVER_NOT_CONNECTED');
|
||||
}
|
||||
foreach ($words as $word) {
|
||||
$this->writeWord($word);
|
||||
}
|
||||
$this->writeWord('');
|
||||
|
||||
$rows = [];
|
||||
while (true) {
|
||||
$sentence = $this->readSentence();
|
||||
$type = array_shift($sentence);
|
||||
if ($type === '!trap' || $type === '!fatal') {
|
||||
throw new RuntimeException('COMMAND_REJECTED: RouterOS menolak operasi.');
|
||||
}
|
||||
if ($type === '!re') {
|
||||
$rows[] = $this->attributes($sentence);
|
||||
}
|
||||
if ($type === '!done') {
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function writeWord(string $word): void
|
||||
{
|
||||
$length = strlen($word);
|
||||
$prefix = match (true) {
|
||||
$length < 0x80 => chr($length),
|
||||
$length < 0x4000 => pack('n', $length | 0x8000),
|
||||
$length < 0x200000 => substr(pack('N', $length | 0xC0000000), 1),
|
||||
$length < 0x10000000 => pack('N', $length | 0xE0000000),
|
||||
default => chr(0xF0).pack('N', $length),
|
||||
};
|
||||
$this->writeAll($prefix.$word);
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function readSentence(): array
|
||||
{
|
||||
$words = [];
|
||||
while (($word = $this->readWord()) !== '') {
|
||||
$words[] = $word;
|
||||
}
|
||||
|
||||
return $words;
|
||||
}
|
||||
|
||||
private function readWord(): string
|
||||
{
|
||||
$length = $this->readLength();
|
||||
|
||||
return $length === 0 ? '' : $this->readBytes($length);
|
||||
}
|
||||
|
||||
private function readLength(): int
|
||||
{
|
||||
$first = ord($this->readBytes(1));
|
||||
if (($first & 0x80) === 0) {
|
||||
return $first;
|
||||
}
|
||||
if (($first & 0xC0) === 0x80) {
|
||||
return (($first & 0x3F) << 8) + ord($this->readBytes(1));
|
||||
}
|
||||
if (($first & 0xE0) === 0xC0) {
|
||||
$bytes = $this->readBytes(2);
|
||||
|
||||
return (($first & 0x1F) << 16) + (ord($bytes[0]) << 8) + ord($bytes[1]);
|
||||
}
|
||||
if (($first & 0xF0) === 0xE0) {
|
||||
$bytes = $this->readBytes(3);
|
||||
|
||||
return (($first & 0x0F) << 24) + (ord($bytes[0]) << 16) + (ord($bytes[1]) << 8) + ord($bytes[2]);
|
||||
}
|
||||
|
||||
return unpack('N', $this->readBytes(4))[1];
|
||||
}
|
||||
|
||||
private function readBytes(int $length): string
|
||||
{
|
||||
$data = '';
|
||||
while (strlen($data) < $length) {
|
||||
$chunk = fread($this->socket, $length - strlen($data));
|
||||
if ($chunk === false || $chunk === '') {
|
||||
throw new RuntimeException('CONNECTION_TIMEOUT: respons RouterOS tidak lengkap.');
|
||||
}
|
||||
$data .= $chunk;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function writeAll(string $data): void
|
||||
{
|
||||
while ($data !== '') {
|
||||
$written = fwrite($this->socket, $data);
|
||||
if ($written === false || $written === 0) {
|
||||
throw new RuntimeException('CONNECTION_FAILED');
|
||||
}
|
||||
$data = substr($data, $written);
|
||||
}
|
||||
}
|
||||
|
||||
private function attributes(array $words): array
|
||||
{
|
||||
$attributes = [];
|
||||
foreach ($words as $word) {
|
||||
if (str_starts_with($word, '=')) {
|
||||
[, $key, $value] = array_pad(explode('=', $word, 3), 3, '');
|
||||
$attributes[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Network\Contracts;
|
||||
|
||||
interface DeviceDriverInterface
|
||||
{
|
||||
public function testConnection(): array;
|
||||
|
||||
public function provisionBaseAccess(string $username, string $password): array;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Network\Drivers\Mikrotik;
|
||||
|
||||
use App\Network\Clients\RouterOs\RouterOsApiClient;
|
||||
use App\Network\Contracts\DeviceDriverInterface;
|
||||
|
||||
class MikrotikDriver implements DeviceDriverInterface
|
||||
{
|
||||
private const GROUPS = [
|
||||
'RADIQ-READ' => 'local,ssh,read,test,winbox,api',
|
||||
'RADIQ-WRITE' => 'local,ssh,read,write,test,winbox,password,api',
|
||||
'RADIQ-NOC' => 'local,ssh,read,write,test,winbox,password,api',
|
||||
'RADIQ-MANAGER' => 'local,ssh,read,write,policy,test,winbox,password,sensitive,api',
|
||||
];
|
||||
|
||||
public function __construct(private readonly RouterOsApiClient $client) {}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
$this->client->connect();
|
||||
try {
|
||||
return $this->client->command(['/system/identity/print', '=.proplist=name'])[0] ?? [];
|
||||
} finally {
|
||||
$this->client->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public function provisionBaseAccess(string $username, string $password): array
|
||||
{
|
||||
$this->client->connect();
|
||||
try {
|
||||
foreach (self::GROUPS as $name => $policies) {
|
||||
$existing = $this->client->command(['/user/group/print', '?name='.$name, '=.proplist=.id']);
|
||||
if ($existing === []) {
|
||||
$this->client->command(['/user/group/add', '=name='.$name, '=policy='.$policies, '=comment=Managed by RADIQ NDM']);
|
||||
} else {
|
||||
$this->client->command(['/user/group/set', '=.id='.$existing[0]['.id'], '=policy='.$policies, '=comment=Managed by RADIQ NDM']);
|
||||
}
|
||||
}
|
||||
|
||||
$users = $this->client->command(['/user/print', '?name='.$username, '=.proplist=.id']);
|
||||
if ($users === []) {
|
||||
$this->client->command(['/user/add', '=name='.$username, '=password='.$password, '=group=RADIQ-MANAGER', '=disabled=no', '=comment=Managed by RADIQ NDM']);
|
||||
} else {
|
||||
$this->client->command(['/user/set', '=.id='.$users[0]['.id'], '=password='.$password, '=group=RADIQ-MANAGER', '=disabled=no', '=comment=Managed by RADIQ NDM']);
|
||||
}
|
||||
|
||||
return ['groups' => array_keys(self::GROUPS), 'username' => $username];
|
||||
} finally {
|
||||
$this->client->disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
|
||||
class TenantPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->can('tenant.view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return $user->can('tenant.view') && ($user->tenant_id === $tenant->id || $user->is_platform_admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->is_platform_admin && $user->can('tenant.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return $user->can('tenant.update') && ($user->tenant_id === $tenant->id || $user->is_platform_admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return $user->is_platform_admin && $user->can('tenant.delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Support\TenantContext;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(TenantContext::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Gate::before(fn ($user): ?bool => $user->is_platform_admin && config('deployment.mode') !== 'self_hosted' ? true : null);
|
||||
|
||||
$this->configureDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure default behaviors for production-ready applications.
|
||||
*/
|
||||
protected function configureDefaults(): void
|
||||
{
|
||||
Date::use(CarbonImmutable::class);
|
||||
|
||||
DB::prohibitDestructiveCommands(
|
||||
app()->isProduction(),
|
||||
);
|
||||
|
||||
Password::defaults(fn (): ?Password => app()->isProduction()
|
||||
? Password::min(12)
|
||||
->mixedCase()
|
||||
->letters()
|
||||
->numbers()
|
||||
->symbols()
|
||||
->uncompromised()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use App\Models\User;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
class FortifyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureActions();
|
||||
$this->configureViews();
|
||||
$this->configureRateLimiting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Fortify actions.
|
||||
*/
|
||||
private function configureActions(): void
|
||||
{
|
||||
Fortify::authenticateUsing(function (Request $request): ?User {
|
||||
$user = User::query()->withoutGlobalScope('tenant')->where('email', $request->string('email'))->first();
|
||||
|
||||
return $user && $user->is_active && Hash::check($request->string('password'), $user->password)
|
||||
? $user
|
||||
: null;
|
||||
});
|
||||
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Fortify views.
|
||||
*/
|
||||
private function configureViews(): void
|
||||
{
|
||||
Fortify::loginView(fn (Request $request) => Inertia::render('auth/login', [
|
||||
'canResetPassword' => Features::enabled(Features::resetPasswords()),
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/reset-password', [
|
||||
'email' => $request->email,
|
||||
'token' => $request->route('token'),
|
||||
'passwordRules' => Password::defaults()->toPasswordRulesString(),
|
||||
]));
|
||||
|
||||
Fortify::requestPasswordResetLinkView(fn (Request $request) => Inertia::render('auth/forgot-password', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::verifyEmailView(fn (Request $request) => Inertia::render('auth/verify-email', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge'));
|
||||
|
||||
Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure rate limiting.
|
||||
*/
|
||||
private function configureRateLimiting(): void
|
||||
{
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||
});
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||
|
||||
return Limit::perMinute(5)->by($throttleKey);
|
||||
});
|
||||
|
||||
RateLimiter::for('passkeys', function (Request $request) {
|
||||
return Limit::perMinute(10)->by(
|
||||
($request->input('credential.id') ?: $request->session()->getId()).'|'.$request->ip(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Devices;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\TenantDeviceSetting;
|
||||
use App\Network\Clients\RouterOs\RouterOsApiClient;
|
||||
use App\Network\Drivers\Mikrotik\MikrotikDriver;
|
||||
use RuntimeException;
|
||||
|
||||
class MikrotikActivationService
|
||||
{
|
||||
public function activate(Device $device): array
|
||||
{
|
||||
abort_unless($device->vendor?->slug === 'mikrotik' && $device->connection_type === 'routeros_api', 422, 'Aktivasi otomatis saat ini hanya mendukung MikroTik RouterOS API.');
|
||||
$setting = TenantDeviceSetting::where('tenant_id', $device->tenant_id)->first();
|
||||
if (! $setting) {
|
||||
throw new RuntimeException('BASE_SETTING_REQUIRED: atur Base User dan Password terlebih dahulu.');
|
||||
}
|
||||
$credential = $device->credentials()->where('is_active', true)->orderByDesc('is_master')->first();
|
||||
if (! $credential) {
|
||||
throw new RuntimeException('CREDENTIAL_REQUIRED: tambahkan credential login perangkat terlebih dahulu.');
|
||||
}
|
||||
|
||||
$client = new RouterOsApiClient($device->management_address, $device->management_port, $credential->username, $credential->password, $setting->connection_timeout, $setting->use_tls, $setting->verify_tls);
|
||||
|
||||
return (new MikrotikDriver($client))->provisionBaseAccess($setting->base_username, $setting->base_password);
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Encryption;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantEncryptionKey;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
class TenantEnvelopeEncryption
|
||||
{
|
||||
private const PREFIX = 'radiq:v1:';
|
||||
|
||||
public function ensureKey(int $tenantId, ?int $actorId = null): TenantEncryptionKey
|
||||
{
|
||||
return TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $tenantId)->where('is_active', true)->first()
|
||||
?? $this->createKey($tenantId, null, $actorId);
|
||||
}
|
||||
|
||||
public function encrypt(int $tenantId, string $plaintext, ?TenantEncryptionKey $key = null): string
|
||||
{
|
||||
$key ??= $this->ensureKey($tenantId);
|
||||
$dek = Crypt::decryptString($key->wrapped_key);
|
||||
$nonce = random_bytes(12);
|
||||
$tag = '';
|
||||
$ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $dek, OPENSSL_RAW_DATA, $nonce, $tag, (string) $tenantId);
|
||||
if ($ciphertext === false) {
|
||||
throw new RuntimeException('ENCRYPTION_FAILED');
|
||||
}
|
||||
|
||||
return self::PREFIX.$key->id.':'.base64_encode($nonce).':'.base64_encode($tag).':'.base64_encode($ciphertext);
|
||||
}
|
||||
|
||||
public function decrypt(int $tenantId, string $payload): string
|
||||
{
|
||||
if (! str_starts_with($payload, self::PREFIX)) {
|
||||
return Crypt::decryptString($payload);
|
||||
}
|
||||
$parts = explode(':', $payload, 6);
|
||||
if (count($parts) !== 6) {
|
||||
throw new RuntimeException('INVALID_ENCRYPTED_PAYLOAD');
|
||||
}
|
||||
$key = TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $tenantId)->findOrFail((int) $parts[2]);
|
||||
$dek = Crypt::decryptString($key->wrapped_key);
|
||||
$plaintext = openssl_decrypt(base64_decode($parts[5], true), 'aes-256-gcm', $dek, OPENSSL_RAW_DATA, base64_decode($parts[3], true), base64_decode($parts[4], true), (string) $tenantId);
|
||||
if ($plaintext === false) {
|
||||
throw new RuntimeException('DECRYPTION_FAILED');
|
||||
}
|
||||
|
||||
return $plaintext;
|
||||
}
|
||||
|
||||
public function rotate(int $tenantId, ?string $manualKey, int $actorId): TenantEncryptionKey
|
||||
{
|
||||
return DB::transaction(function () use ($tenantId, $manualKey, $actorId): TenantEncryptionKey {
|
||||
Tenant::whereKey($tenantId)->lockForUpdate()->firstOrFail();
|
||||
$oldKeys = TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $tenantId)->lockForUpdate()->get();
|
||||
$newKey = $this->createKey($tenantId, $manualKey, $actorId, ($oldKeys->max('version') ?? 0) + 1);
|
||||
foreach (DB::table('device_credentials')->where('tenant_id', $tenantId)->lockForUpdate()->get(['id', 'password']) as $credential) {
|
||||
DB::table('device_credentials')->where('id', $credential->id)->update(['password' => $this->encrypt($tenantId, $this->decrypt($tenantId, $credential->password), $newKey), 'updated_at' => now()]);
|
||||
}
|
||||
foreach (DB::table('tenant_device_settings')->where('tenant_id', $tenantId)->lockForUpdate()->get(['id', 'base_password']) as $setting) {
|
||||
DB::table('tenant_device_settings')->where('id', $setting->id)->update(['base_password' => $this->encrypt($tenantId, $this->decrypt($tenantId, $setting->base_password), $newKey), 'updated_at' => now()]);
|
||||
}
|
||||
TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $tenantId)->where('id', '!=', $newKey->id)->where('is_active', true)->update(['is_active' => false, 'retired_at' => now()]);
|
||||
|
||||
return $newKey;
|
||||
});
|
||||
}
|
||||
|
||||
private function createKey(int $tenantId, ?string $manualKey, ?int $actorId, int $version = 1): TenantEncryptionKey
|
||||
{
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
$dek = $manualKey === null ? random_bytes(32) : hash_hkdf('sha256', $manualKey, 32, 'RADIQ-NDM:'.$tenant->uuid);
|
||||
|
||||
return TenantEncryptionKey::withoutGlobalScope('tenant')->create([
|
||||
'tenant_id' => $tenantId, 'version' => $version, 'wrapped_key' => Crypt::encryptString($dek),
|
||||
'is_active' => true, 'source' => $manualKey === null ? 'generated' : 'manual', 'created_by' => $actorId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Models\DeploymentInstallation;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantDevicePolicy;
|
||||
use App\Models\User;
|
||||
use App\Services\Encryption\TenantEnvelopeEncryption;
|
||||
use App\Support\SystemRolePermissions;
|
||||
use App\Support\TenantContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class TenantProvisioningService
|
||||
{
|
||||
public function __construct(private readonly TenantContext $context) {}
|
||||
|
||||
/** @param array{name:string,slug:string,is_active?:bool,owner_name:string,owner_email:string,owner_password:string} $data */
|
||||
public function createWithOwner(array $data): Tenant
|
||||
{
|
||||
return DB::transaction(function () use ($data): Tenant {
|
||||
$tenant = Tenant::create([
|
||||
'name' => $data['name'],
|
||||
'slug' => $data['slug'],
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
]);
|
||||
TenantDevicePolicy::create(['tenant_id' => $tenant->id]);
|
||||
app(TenantEnvelopeEncryption::class)->ensureKey($tenant->id);
|
||||
$deploymentType = $data['deployment_type'] ?? 'managed_cloud';
|
||||
DeploymentInstallation::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'name' => $deploymentType === 'self_hosted' ? 'Server Tenant' : 'RADIQ Managed Cloud',
|
||||
'deployment_type' => $deploymentType,
|
||||
'domain' => $data['deployment_domain'] ?? null,
|
||||
'instance_key_hash' => hash('sha256', random_bytes(32)),
|
||||
'status' => $deploymentType === 'managed_cloud' ? 'active' : 'pending',
|
||||
'activated_at' => $deploymentType === 'managed_cloud' ? now() : null,
|
||||
]);
|
||||
|
||||
$this->context->set($tenant->id);
|
||||
setPermissionsTeamId($tenant->id);
|
||||
|
||||
try {
|
||||
$roles = $this->createDefaultRoles($tenant);
|
||||
|
||||
$owner = User::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'name' => $data['owner_name'],
|
||||
'email' => $data['owner_email'],
|
||||
'password' => $data['owner_password'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
$owner->forceFill(['email_verified_at' => now()])->save();
|
||||
$owner->assignRole($roles[SystemRole::TenantAdmin->value]);
|
||||
} finally {
|
||||
$this->context->clear();
|
||||
setPermissionsTeamId(null);
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array<string, Role> */
|
||||
private function createDefaultRoles(Tenant $tenant): array
|
||||
{
|
||||
$definitions = SystemRolePermissions::tenantRoles();
|
||||
|
||||
$roles = [];
|
||||
|
||||
foreach ($definitions as $name => $permissions) {
|
||||
$role = Role::create(['tenant_id' => $tenant->id, 'name' => $name, 'guard_name' => 'web']);
|
||||
$role->syncPermissions($permissions);
|
||||
$roles[$name] = $role;
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
|
||||
final class SystemRolePermissions
|
||||
{
|
||||
/** @return array<string, list<string>> */
|
||||
public static function tenantRoles(): array
|
||||
{
|
||||
return [
|
||||
SystemRole::TenantAdmin->value => [
|
||||
'user.view', 'user.create', 'user.update', 'user.delete',
|
||||
'device.view', 'device.create', 'device.update', 'device.delete', 'device.connect',
|
||||
'device.credential.view', 'device.credential.create', 'device.credential.update', 'device.credential.delete',
|
||||
'device.user.view', 'device.user.create', 'device.user.update', 'device.user.delete', 'device.user.mass_update',
|
||||
'device.master_account.manage', 'device.command.execute', 'device.command.mass_execute',
|
||||
'device.config.view', 'device.config.deploy', 'device.backup.create', 'device.backup.download',
|
||||
'device.backup.restore', 'device.monitoring.view', 'audit.view', 'license.view', 'installation.view',
|
||||
],
|
||||
SystemRole::TenantUser->value => [
|
||||
'device.view', 'device.create', 'device.update', 'device.connect',
|
||||
'device.user.view', 'device.user.create', 'device.user.update',
|
||||
'device.command.execute', 'device.config.view', 'device.backup.create',
|
||||
'device.backup.download', 'device.monitoring.view',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
final class TenantContext
|
||||
{
|
||||
private ?int $tenantId = null;
|
||||
|
||||
public function set(?int $tenantId): void
|
||||
{
|
||||
$this->tenantId = $tenantId;
|
||||
}
|
||||
|
||||
public function id(): ?int
|
||||
{
|
||||
return $this->tenantId;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->tenantId = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user