feat: inisialisasi project kit v2
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class ActivityLogController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('user.view');
|
||||
|
||||
$search = $request->input('search');
|
||||
$logName = $request->input('log_name');
|
||||
$event = $request->input('event');
|
||||
$perPage = (int) $request->input('per_page', 15);
|
||||
|
||||
$query = Activity::with('causer')->latest();
|
||||
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('description', 'like', "%{$search}%")
|
||||
->orWhere('log_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($logName) {
|
||||
$query->where('log_name', $logName);
|
||||
}
|
||||
|
||||
if ($event) {
|
||||
$query->where('event', $event);
|
||||
}
|
||||
|
||||
$activities = $query->paginate($perPage)->withQueryString();
|
||||
|
||||
$logNames = Activity::distinct()->pluck('log_name');
|
||||
$events = Activity::distinct()->whereNotNull('event')->pluck('event');
|
||||
|
||||
return Inertia::render('ActivityLogs/Index', [
|
||||
'activities' => [
|
||||
'data' => $activities->items(),
|
||||
'meta' => [
|
||||
'current_page' => $activities->currentPage(),
|
||||
'last_page' => $activities->lastPage(),
|
||||
'total' => $activities->total(),
|
||||
'per_page' => $activities->perPage(),
|
||||
],
|
||||
'links' => $activities->linkCollection()->toArray(),
|
||||
],
|
||||
'filters' => $request->only(['search', 'log_name', 'event', 'per_page']),
|
||||
'availableLogNames' => $logNames,
|
||||
'availableEvents' => $events,
|
||||
]);
|
||||
}
|
||||
|
||||
public function bulkDelete(Request $request)
|
||||
{
|
||||
$this->authorize('user.delete');
|
||||
|
||||
$ids = (array) $request->input('ids', []);
|
||||
|
||||
Activity::whereIn('id', $ids)->delete();
|
||||
|
||||
return back()->with('success', \count($ids) . ' logs deleted successfully.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* @group Mobile App Config
|
||||
*
|
||||
* Public endpoint for mobile app version checks and maintenance status.
|
||||
*/
|
||||
class AppConfigController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get App Config
|
||||
*
|
||||
* Returns version info and maintenance status for the given platform.
|
||||
*
|
||||
* @unauthenticated
|
||||
* @queryParam platform string The platform (android|ios). Defaults to android.
|
||||
*/
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$platform = $request->input('platform', 'android');
|
||||
if (!\in_array($platform, ['android', 'ios'], true)) {
|
||||
$platform = 'android';
|
||||
}
|
||||
|
||||
$settings = Cache::get('system_settings', function () {
|
||||
try {
|
||||
return Setting::pluck('value', 'key')->toArray();
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
if ($platform === 'android') {
|
||||
$maintenance = filter_var($settings['android_maintenance_mode'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if ($maintenance) {
|
||||
return response()->json([
|
||||
'maintenance' => true,
|
||||
'message' => 'The app is temporarily under maintenance. Please try again later.',
|
||||
], 503);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'maintenance' => false,
|
||||
'latest_version' => $settings['android_latest_version'] ?? '1.0.0',
|
||||
'min_version' => $settings['android_min_version'] ?? '1.0.0',
|
||||
'store_url' => $settings['android_playstore_url'] ?? null,
|
||||
'platform' => 'android',
|
||||
]);
|
||||
}
|
||||
|
||||
// ios — placeholder using same pattern, expandable when ios settings are added
|
||||
return response()->json([
|
||||
'maintenance' => false,
|
||||
'latest_version' => $settings['ios_latest_version'] ?? '1.0.0',
|
||||
'min_version' => $settings['ios_min_version'] ?? '1.0.0',
|
||||
'store_url' => $settings['ios_appstore_url'] ?? null,
|
||||
'platform' => 'ios',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Actions\Auth\LoginAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\UserResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* @group Authentication
|
||||
*
|
||||
* APIs for managing authentication
|
||||
*/
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* Authenticate a user and return a Sanctum token.
|
||||
*
|
||||
* @unauthenticated
|
||||
*/
|
||||
public function login(Request $request, LoginAction $action): JsonResponse
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required',
|
||||
]);
|
||||
|
||||
$result = $action->execute($credentials);
|
||||
|
||||
return response()->json([
|
||||
'data' => new UserResource($result['user']),
|
||||
'token' => $result['token'],
|
||||
'roles' => $result['roles'],
|
||||
'permissions' => $result['permissions'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Current User
|
||||
*
|
||||
* Return the currently authenticated user's details.
|
||||
*/
|
||||
public function me(Request $request): UserResource
|
||||
{
|
||||
return new UserResource($request->user());
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*
|
||||
* Revoke the current user's token.
|
||||
*/
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
|
||||
return response()->json(['message' => 'Logged out successfully']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Actions\Users\CreateUserAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
/**
|
||||
* @group User Management
|
||||
*
|
||||
* APIs for managing users
|
||||
*/
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* List Users
|
||||
*
|
||||
* Get a paginated list of users.
|
||||
*/
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$this->authorize('user.view');
|
||||
|
||||
$users = User::query()
|
||||
->when($request->search, function ($query, $search) {
|
||||
$query->where('first_name', 'like', "%{$search}%")
|
||||
->orWhere('last_name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
})
|
||||
->paginate($request->perPage ?? 15);
|
||||
|
||||
return UserResource::collection($users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create User
|
||||
*
|
||||
* Create a new user with roles.
|
||||
*/
|
||||
public function store(Request $request, CreateUserAction $action): UserResource
|
||||
{
|
||||
$this->authorize('user.create');
|
||||
|
||||
$validated = $request->validate([
|
||||
'firstName' => 'required|string|max:100',
|
||||
'lastName' => 'required|string|max:100',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|min:8',
|
||||
'status' => 'string|in:active,inactive,suspended',
|
||||
'roles' => 'array',
|
||||
]);
|
||||
|
||||
$user = $action->execute($validated);
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get User
|
||||
*
|
||||
* Get details of a specific user.
|
||||
*/
|
||||
public function show(User $user): UserResource
|
||||
{
|
||||
$this->authorize('user.view');
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update User
|
||||
*
|
||||
* Update a user's details.
|
||||
*/
|
||||
public function update(Request $request, User $user): UserResource
|
||||
{
|
||||
$this->authorize('user.edit');
|
||||
|
||||
$validated = $request->validate([
|
||||
'firstName' => 'string|max:100',
|
||||
'lastName' => 'string|max:100',
|
||||
'email' => 'email|unique:users,email,' . $user->id,
|
||||
'status' => 'string|in:active,inactive,suspended',
|
||||
]);
|
||||
|
||||
// Mapping camelCase to snake_case for DB
|
||||
if (isset($validated['firstName'])) $user->first_name = $validated['firstName'];
|
||||
if (isset($validated['lastName'])) $user->last_name = $validated['lastName'];
|
||||
if (isset($validated['email'])) $user->email = $validated['email'];
|
||||
if (isset($validated['status'])) $user->status = $validated['status'];
|
||||
|
||||
$user->save();
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete User
|
||||
*
|
||||
* Soft delete a user.
|
||||
*/
|
||||
public function destroy(User $user): JsonResponse
|
||||
{
|
||||
$this->authorize('user.delete');
|
||||
|
||||
$user->delete();
|
||||
|
||||
return response()->json(['message' => 'User deleted successfully']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('Auth/Login', [
|
||||
'canResetPassword' => Route::has('password.request'),
|
||||
'status' => session('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
// If user has 2FA enabled, redirect to challenge screen
|
||||
if ($user->two_factor_confirmed_at && $user->two_factor_secret) {
|
||||
$request->session()->put('two_factor_user_id', $user->id);
|
||||
Auth::guard('web')->logout();
|
||||
$request->session()->forget('password_hash_web');
|
||||
|
||||
return redirect()->route('two-factor.challenge');
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the confirm password view.
|
||||
*/
|
||||
public function show(): Response
|
||||
{
|
||||
return Inertia::render('Auth/ConfirmPassword');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the email verification prompt.
|
||||
*/
|
||||
public function __invoke(Request $request): RedirectResponse|Response
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(route('dashboard', absolute: false))
|
||||
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset view.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('Auth/ResetPassword', [
|
||||
'email' => $request->email,
|
||||
'token' => $request->route('token'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
if ($status == Password::PASSWORD_RESET) {
|
||||
return redirect()->route('login')->with('status', __($status));
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [trans($status)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset link request view.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('Auth/ForgotPassword', [
|
||||
'status' => session('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
if ($status == Password::RESET_LINK_SENT) {
|
||||
return back()->with('status', __($status));
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [trans($status)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
$settings = \Illuminate\Support\Facades\Cache::get('system_settings', []);
|
||||
abort_if(($settings['allow_registration'] ?? '1') === '0', 403, 'Registration is currently disabled.');
|
||||
|
||||
return Inertia::render('Auth/Register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$settings = \Illuminate\Support\Facades\Cache::get('system_settings', []);
|
||||
abort_if(($settings['allow_registration'] ?? '1') === '0', 403, 'Registration is currently disabled.');
|
||||
|
||||
$request->validate([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'first_name' => $request->first_name,
|
||||
'last_name' => $request->last_name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// General Stats
|
||||
$totalUsers = User::count();
|
||||
$activeUsers = User::where('status', 'active')->count();
|
||||
$totalRoles = Role::count();
|
||||
$recentUsers = User::with('roles')->latest()->take(5)->get();
|
||||
|
||||
$driver = DB::connection()->getDriverName();
|
||||
|
||||
// Chart Data: User Growth (Last 6 Months)
|
||||
if ($driver === 'sqlite') {
|
||||
$monthFormat = "strftime('%Y-%m', created_at)";
|
||||
} elseif ($driver === 'pgsql') {
|
||||
$monthFormat = "to_char(created_at, 'YYYY-MM')";
|
||||
} else {
|
||||
$monthFormat = "DATE_FORMAT(created_at, '%Y-%m')";
|
||||
}
|
||||
|
||||
$userGrowth = User::select(
|
||||
DB::raw('count(id) as total'),
|
||||
DB::raw("$monthFormat as month")
|
||||
)
|
||||
->groupBy('month')
|
||||
->orderBy('month', 'asc')
|
||||
->take(6)
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'label' => Carbon::parse($item->month . '-01')->format('M Y'),
|
||||
'value' => $item->total,
|
||||
];
|
||||
});
|
||||
|
||||
// Chart Data: Activity Logs (Last 7 Days)
|
||||
if ($driver === 'sqlite') {
|
||||
$dateFormat = "strftime('%Y-%m-%d', created_at)";
|
||||
} elseif ($driver === 'pgsql') {
|
||||
$dateFormat = "to_char(created_at, 'YYYY-MM-DD')";
|
||||
} else {
|
||||
$dateFormat = "DATE(created_at)";
|
||||
}
|
||||
|
||||
$activityStats = [];
|
||||
if (Schema::hasTable('activity_log')) {
|
||||
$activityStats = DB::table('activity_log')
|
||||
->select(
|
||||
DB::raw('count(id) as total'),
|
||||
DB::raw("$dateFormat as date")
|
||||
)
|
||||
->where('created_at', '>=', Carbon::now()->subDays(7))
|
||||
->groupBy('date')
|
||||
->orderBy('date', 'asc')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'label' => Carbon::parse($item->date)->format('D, M d'),
|
||||
'value' => $item->total,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'stats' => [
|
||||
'totalUsers' => $totalUsers,
|
||||
'activeUsers' => $activeUsers,
|
||||
'totalRoles' => $totalRoles,
|
||||
'recentUsers' => $recentUsers,
|
||||
'charts' => [
|
||||
'userGrowth' => $userGrowth,
|
||||
'activityStats' => $activityStats,
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DocsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GlobalSearchController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$query = $request->input('query');
|
||||
|
||||
if (empty($query)) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
// Search Users
|
||||
$users = User::where('first_name', 'like', "%{$query}%")
|
||||
->orWhere('last_name', 'like', "%{$query}%")
|
||||
->orWhere('email', 'like', "%{$query}%")
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn($u) => [
|
||||
'type' => 'User',
|
||||
'title' => "{$u->first_name} {$u->last_name}",
|
||||
'subtitle' => $u->email,
|
||||
'url' => route('users.show', $u->id),
|
||||
'icon' => 'user'
|
||||
]);
|
||||
$results = array_merge($results, $users->toArray());
|
||||
|
||||
// Search Roles
|
||||
$roles = Role::where('name', 'like', "%{$query}%")
|
||||
->limit(3)
|
||||
->get()
|
||||
->map(fn($r) => [
|
||||
'type' => 'Role',
|
||||
'title' => $r->name,
|
||||
'subtitle' => "{$r->permissions()->count()} permissions",
|
||||
'url' => route('roles.index'),
|
||||
'icon' => 'shield'
|
||||
]);
|
||||
$results = array_merge($results, $roles->toArray());
|
||||
|
||||
// Search Activity Logs
|
||||
$logs = Activity::where('description', 'like', "%{$query}%")
|
||||
->orWhere('log_name', 'like', "%{$query}%")
|
||||
->latest()
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn($l) => [
|
||||
'type' => 'Log',
|
||||
'title' => $l->description,
|
||||
'subtitle' => $l->created_at->diffForHumans(),
|
||||
'url' => route('activity-logs.index'),
|
||||
'icon' => 'clock'
|
||||
]);
|
||||
$results = array_merge($results, $logs->toArray());
|
||||
|
||||
// Search Navigation Pages
|
||||
$pages = [
|
||||
['title' => 'Dashboard', 'url' => route('dashboard'), 'icon' => 'clock', 'keywords' => ['home', 'index', 'dashboard']],
|
||||
['title' => 'User Management', 'url' => route('users.index'), 'icon' => 'user', 'keywords' => ['users', 'people', 'staff', 'accounts']],
|
||||
['title' => 'Role Management', 'url' => route('roles.index'), 'icon' => 'shield', 'keywords' => ['roles', 'permissions', 'access', 'security']],
|
||||
['title' => 'Activity Logs', 'url' => route('activity-logs.index'), 'icon' => 'clock', 'keywords' => ['logs', 'audit', 'history', 'events']],
|
||||
['title' => 'System Settings', 'url' => route('system.settings.index'), 'icon' => 'shield', 'keywords' => ['settings', 'config', 'setup', 'system']],
|
||||
['title' => 'Profile Settings', 'url' => route('profile.edit'), 'icon' => 'user', 'keywords' => ['profile', 'me', 'account', 'password']],
|
||||
];
|
||||
|
||||
$matchedPages = array_filter($pages, function($page) use ($query) {
|
||||
$q = strtolower($query);
|
||||
if (str_contains(strtolower($page['title']), $q)) return true;
|
||||
foreach ($page['keywords'] as $keyword) {
|
||||
if (str_contains($keyword, $q)) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
foreach ($matchedPages as $page) {
|
||||
$results[] = [
|
||||
'type' => 'Page',
|
||||
'title' => $page['title'],
|
||||
'subtitle' => 'System Navigation',
|
||||
'url' => $page['url'],
|
||||
'icon' => $page['icon']
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json($results);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\NotificationLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$logs = NotificationLog::with(['targetUser', 'sender'])
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
$users = User::select('id', 'first_name', 'last_name', 'email')
|
||||
->where('status', 'active')
|
||||
->get();
|
||||
|
||||
return Inertia::render('Notifications/Index', [
|
||||
'logs' => [
|
||||
'data' => $logs->items(),
|
||||
'meta' => [
|
||||
'current_page' => $logs->currentPage(),
|
||||
'last_page' => $logs->lastPage(),
|
||||
'total' => $logs->total(),
|
||||
'per_page' => $logs->perPage(),
|
||||
],
|
||||
'links' => $logs->linkCollection()->toArray(),
|
||||
],
|
||||
'users' => $users,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'body' => 'required|string',
|
||||
'image_url' => 'nullable|url',
|
||||
'deep_link' => 'nullable|string',
|
||||
'target_type' => 'required|in:all,individual',
|
||||
'target_user_id' => 'required_if:target_type,individual|nullable|exists:users,id',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Mocking FCM Sending logic
|
||||
// In production, use Kreait/Firebase-PHP or Laravel-Notification-Channels/WebPush
|
||||
$status = 'sent';
|
||||
$errorMessage = null;
|
||||
|
||||
// Log the notification
|
||||
NotificationLog::create([
|
||||
'title' => $validated['title'],
|
||||
'body' => $validated['body'],
|
||||
'image_url' => $validated['image_url'],
|
||||
'deep_link' => $validated['deep_link'],
|
||||
'target_type' => $validated['target_type'],
|
||||
'target_user_id' => $validated['target_user_id'],
|
||||
'sender_id' => auth()->id(),
|
||||
'status' => $status,
|
||||
'error_message' => $errorMessage,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Notification dispatched successfully.');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('FCM Error: ' . $e->getMessage());
|
||||
return back()->with('error', 'Failed to send notification: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile form (Redirect to consolidated settings).
|
||||
*/
|
||||
public function edit(Request $request): RedirectResponse
|
||||
{
|
||||
return Redirect::route('settings.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information and avatar.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$user->fill($request->validated());
|
||||
|
||||
if ($user->isDirty('email')) {
|
||||
$user->email_verified_at = null;
|
||||
}
|
||||
|
||||
// Handle Avatar Upload
|
||||
if ($request->hasFile('avatar_file')) {
|
||||
// Delete old avatar if exists
|
||||
if ($user->avatar_url) {
|
||||
$oldPath = str_replace('/storage/', '', $user->avatar_url);
|
||||
Storage::disk('public')->delete($oldPath);
|
||||
}
|
||||
|
||||
$path = $request->file('avatar_file')->store('avatars', 'public');
|
||||
$user->avatar_url = Storage::url($path);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return Redirect::route('settings.index')->with('status', 'profile-updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's account.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return Redirect::to('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$order = ['super-admin' => 0, 'admin' => 1, 'user' => 2];
|
||||
|
||||
$roles = Role::where('guard_name', 'web')
|
||||
->with('permissions')
|
||||
->get()
|
||||
->map(function ($role) {
|
||||
return [
|
||||
'id' => $role->id,
|
||||
'name' => $role->name,
|
||||
'guard_name' => $role->guard_name,
|
||||
'permissions' => $role->permissions->pluck('name')->toArray(),
|
||||
'users_count' => $role->users()->count(),
|
||||
'created_at' => $role->created_at,
|
||||
];
|
||||
})
|
||||
->sortBy(fn ($role) => $order[$role['name']] ?? 99)
|
||||
->values();
|
||||
|
||||
$permissions = Permission::where('guard_name', 'web')
|
||||
->get()
|
||||
->map(fn($p) => [
|
||||
'id' => $p->id,
|
||||
'name' => $p->name,
|
||||
'group' => explode('.', $p->name)[0] ?? 'other',
|
||||
]);
|
||||
|
||||
return Inertia::render('Roles/Index', [
|
||||
'roles' => $roles,
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the permissions matrix for a role.
|
||||
*/
|
||||
public function updatePermissions(Request $request, Role $role)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'permissions' => 'required|array',
|
||||
'permissions.*' => 'string|exists:permissions,name',
|
||||
]);
|
||||
|
||||
// Sync only web guard permissions
|
||||
$role->syncPermissions($validated['permissions']);
|
||||
|
||||
return back()->with('success', "Permissions updated for role '{$role->name}'.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new role.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:50|unique:roles,name',
|
||||
]);
|
||||
|
||||
Role::create([
|
||||
'name' => $validated['name'],
|
||||
'guard_name' => 'web',
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Role created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role.
|
||||
*/
|
||||
public function destroy(Role $role)
|
||||
{
|
||||
if ($role->name === 'super-admin') {
|
||||
return back()->withErrors(['error' => 'Cannot delete the super-admin role.']);
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
|
||||
return back()->with('success', 'Role deleted successfully.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the consolidated account settings page.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$twoFactorEnabled = !is_null($user->two_factor_confirmed_at);
|
||||
|
||||
$qrCode = null;
|
||||
$secret = null;
|
||||
|
||||
if (!$twoFactorEnabled) {
|
||||
if (!$user->two_factor_secret) {
|
||||
$g2fa = new \PragmaRX\Google2FA\Google2FA();
|
||||
$user->update(['two_factor_secret' => $g2fa->generateSecretKey()]);
|
||||
}
|
||||
$secret = $user->fresh()->two_factor_secret;
|
||||
$g2fa = new \PragmaRX\Google2FA\Google2FA();
|
||||
$otpUrl = $g2fa->getQRCodeUrl(config('app.name'), $user->email, $secret);
|
||||
$renderer = new \BaconQrCode\Renderer\ImageRenderer(
|
||||
new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200),
|
||||
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
|
||||
);
|
||||
$qrCode = 'data:image/svg+xml;base64,' . base64_encode((new \BaconQrCode\Writer($renderer))->writeString($otpUrl));
|
||||
}
|
||||
|
||||
return Inertia::render('Settings/Index', [
|
||||
'mustVerifyEmail' => $user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail,
|
||||
'status' => session('status'),
|
||||
'twoFactor' => [
|
||||
'enabled' => $twoFactorEnabled,
|
||||
'qr_code' => $qrCode,
|
||||
'secret' => $secret,
|
||||
'recovery_codes' => $user->two_factor_recovery_codes
|
||||
? json_decode($user->two_factor_recovery_codes, true)
|
||||
: [],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SystemSettingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the system settings page.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
abort_if(!auth()->user()->hasRole('super-admin'), 403, 'Unauthorized. Super-Admin only.');
|
||||
|
||||
$settings = Setting::all()->pluck('value', 'key');
|
||||
|
||||
$defaultSettings = [
|
||||
'app_name' => 'biiproject kit v2',
|
||||
'app_logo' => null,
|
||||
'app_logo_text' => 'BK',
|
||||
'app_description' => 'Enterprise Admin Platform',
|
||||
'allow_registration' => '1',
|
||||
'require_email_verification' => '0',
|
||||
'password_minimum_length' => '8',
|
||||
'password_require_symbols' => '0',
|
||||
'password_require_numbers' => '0',
|
||||
'password_require_mixed_case' => '0',
|
||||
|
||||
// OAuth
|
||||
'oauth_google_enabled' => '0',
|
||||
'oauth_google_client_id' => '',
|
||||
'oauth_google_client_secret' => '',
|
||||
'oauth_github_enabled' => '0',
|
||||
'oauth_github_client_id' => '',
|
||||
'oauth_github_client_secret' => '',
|
||||
|
||||
// SMTP
|
||||
'mail_host' => 'smtp.mailtrap.io',
|
||||
'mail_port' => '2525',
|
||||
'mail_username' => '',
|
||||
'mail_password' => '',
|
||||
'mail_encryption' => 'tls',
|
||||
'mail_from_address' => 'hello@biiproject.com',
|
||||
'mail_from_name' => 'biiproject kit Admin',
|
||||
|
||||
'primary_color' => '#6366f1',
|
||||
|
||||
// Mobile App Control
|
||||
'android_latest_version' => '1.0.0',
|
||||
'android_min_version' => '1.0.0',
|
||||
'android_maintenance_mode' => '0',
|
||||
'android_playstore_url' => '',
|
||||
];
|
||||
|
||||
$mergedSettings = array_merge($defaultSettings, $settings->toArray());
|
||||
|
||||
return Inertia::render('SystemSettings/Index', [
|
||||
'settings' => $mergedSettings,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the system settings.
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
abort_if(!auth()->user()->hasRole('super-admin'), 403, 'Unauthorized. Super-Admin only.');
|
||||
|
||||
$validated = $request->validate([
|
||||
'settings' => 'required|array',
|
||||
'settings.app_name' => 'required|string|max:255',
|
||||
'settings.app_logo_text' => 'required|string|max:3',
|
||||
'settings.app_description' => 'nullable|string',
|
||||
'settings.allow_registration' => 'boolean',
|
||||
'settings.require_email_verification' => 'boolean',
|
||||
'settings.password_minimum_length' => 'integer|min:8',
|
||||
'settings.password_require_symbols' => 'boolean',
|
||||
'settings.password_require_numbers' => 'boolean',
|
||||
'settings.password_require_mixed_case' => 'boolean',
|
||||
|
||||
'settings.oauth_google_enabled' => 'boolean',
|
||||
'settings.oauth_google_client_id' => 'nullable|string',
|
||||
'settings.oauth_google_client_secret' => 'nullable|string',
|
||||
'settings.oauth_github_enabled' => 'boolean',
|
||||
'settings.oauth_github_client_id' => 'nullable|string',
|
||||
'settings.oauth_github_client_secret' => 'nullable|string',
|
||||
|
||||
'settings.mail_host' => 'nullable|string',
|
||||
'settings.mail_port' => 'nullable|string',
|
||||
'settings.mail_username' => 'nullable|string',
|
||||
'settings.mail_password' => 'nullable|string',
|
||||
'settings.mail_encryption' => 'nullable|string',
|
||||
'settings.mail_from_address' => 'nullable|email',
|
||||
'settings.mail_from_name' => 'nullable|string',
|
||||
|
||||
'settings.primary_color' => 'required|string',
|
||||
|
||||
// Mobile App Control
|
||||
'settings.android_latest_version' => 'nullable|string',
|
||||
'settings.android_min_version' => 'nullable|string',
|
||||
'settings.android_maintenance_mode' => 'boolean',
|
||||
'settings.android_playstore_url' => 'nullable|url',
|
||||
'logo_file' => 'nullable|image|max:2048',
|
||||
]);
|
||||
|
||||
$settings = $validated['settings'];
|
||||
|
||||
if ($request->hasFile('logo_file')) {
|
||||
$path = $request->file('logo_file')->store('branding', 'public');
|
||||
$logoUrl = Storage::url($path);
|
||||
Setting::updateOrCreate(['key' => 'app_logo'], ['value' => $logoUrl, 'type' => 'string']);
|
||||
}
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
Setting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => (string) $value, 'type' => is_bool($value) ? 'boolean' : (is_int($value) ? 'integer' : 'string')]
|
||||
);
|
||||
}
|
||||
|
||||
Cache::forget('system_settings');
|
||||
|
||||
return back()->with('success', 'System configurations updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a test email using SMTP details from the request.
|
||||
*/
|
||||
public function testEmail(Request $request)
|
||||
{
|
||||
abort_if(!auth()->user()->hasRole('super-admin'), 403, 'Unauthorized. Super-Admin only.');
|
||||
|
||||
$request->validate([
|
||||
'recipient' => 'required|email',
|
||||
'mail_host' => 'nullable|string',
|
||||
'mail_port' => 'nullable|string',
|
||||
'mail_username' => 'nullable|string',
|
||||
'mail_password' => 'nullable|string',
|
||||
'mail_encryption' => 'nullable|string',
|
||||
'mail_from_address' => 'nullable|email',
|
||||
'mail_from_name' => 'nullable|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Apply SMTP settings at runtime
|
||||
config([
|
||||
'mail.default' => 'smtp',
|
||||
'mail.mailers.smtp.transport' => 'smtp',
|
||||
'mail.mailers.smtp.host' => $request->input('mail_host'),
|
||||
'mail.mailers.smtp.port' => $request->input('mail_port'),
|
||||
'mail.mailers.smtp.username' => $request->input('mail_username'),
|
||||
'mail.mailers.smtp.password' => $request->input('mail_password'),
|
||||
'mail.mailers.smtp.encryption' => $request->input('mail_encryption') ?: 'tls',
|
||||
'mail.from.address' => $request->input('mail_from_address'),
|
||||
'mail.from.name' => $request->input('mail_from_name'),
|
||||
]);
|
||||
|
||||
\Illuminate\Support\Facades\Mail::raw("Halo!\n\nIni adalah email uji coba (test email) dari sistem biiskit.\n\nJika Anda menerima email ini, berarti konfigurasi SMTP Anda berfungsi dengan baik!\n\nDetail Konfigurasi:\n- Host: " . ($request->input('mail_host') ?: '-') . "\n- Port: " . ($request->input('mail_port') ?: '-') . "\n- Sender: " . ($request->input('mail_from_address') ?: '-') . "\n\nSalam,\nAdmin biiskit", function ($message) use ($request) {
|
||||
$message->to($request->input('recipient'))
|
||||
->subject("Uji Coba Konfigurasi SMTP - biiskit");
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Email uji coba telah berhasil dikirim ke ' . $request->input('recipient')
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal mengirim email uji coba. Error: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TwoFactorController extends Controller
|
||||
{
|
||||
protected Google2FA $google2fa;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->google2fa = new Google2FA();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show 2FA setup page (generate QR code data).
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
// If not yet set up, generate a new secret
|
||||
if (!$user->two_factor_secret) {
|
||||
$secret = $this->google2fa->generateSecretKey();
|
||||
$user->update(['two_factor_secret' => $secret]);
|
||||
}
|
||||
|
||||
$secret = $user->two_factor_secret;
|
||||
|
||||
$otpUrl = $this->google2fa->getQRCodeUrl(
|
||||
config('app.name'),
|
||||
$user->email,
|
||||
$secret
|
||||
);
|
||||
|
||||
// Generate QR code as SVG using BaconQrCode
|
||||
$renderer = new \BaconQrCode\Renderer\ImageRenderer(
|
||||
new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200),
|
||||
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
|
||||
);
|
||||
$qrCode = (new \BaconQrCode\Writer($renderer))->writeString($otpUrl);
|
||||
$qrCodeBase64 = 'data:image/svg+xml;base64,' . base64_encode($qrCode);
|
||||
|
||||
return Inertia::render('TwoFactor/Setup', [
|
||||
'enabled' => !is_null($user->two_factor_confirmed_at),
|
||||
'qr_code' => $qrCodeBase64,
|
||||
'secret' => $secret,
|
||||
'recovery_codes' => $user->two_factor_recovery_codes
|
||||
? json_decode($user->two_factor_recovery_codes, true)
|
||||
: [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm & enable 2FA.
|
||||
*/
|
||||
public function enable(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'code' => 'required|string',
|
||||
]);
|
||||
|
||||
$user = auth()->user();
|
||||
$secret = $user->two_factor_secret;
|
||||
|
||||
$valid = $this->google2fa->verifyKey($secret, $request->code);
|
||||
|
||||
if (!$valid) {
|
||||
return back()->withErrors(['code' => 'Invalid authentication code. Please try again.']);
|
||||
}
|
||||
|
||||
// Generate recovery codes
|
||||
$recoveryCodes = Collection::times(8, fn() => Str::random(10) . '-' . Str::random(10));
|
||||
|
||||
$user->update([
|
||||
'two_factor_confirmed_at' => now(),
|
||||
'two_factor_recovery_codes' => json_encode($recoveryCodes->toArray()),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Two-Factor Authentication has been enabled successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable 2FA.
|
||||
*/
|
||||
public function disable(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'password' => 'required|current_password',
|
||||
]);
|
||||
|
||||
auth()->user()->update([
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'two_factor_confirmed_at' => null,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Two-Factor Authentication has been disabled.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate recovery codes.
|
||||
*/
|
||||
public function regenerateCodes()
|
||||
{
|
||||
$recoveryCodes = Collection::times(8, fn() => Str::random(10) . '-' . Str::random(10));
|
||||
|
||||
auth()->user()->update([
|
||||
'two_factor_recovery_codes' => json_encode($recoveryCodes->toArray()),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Recovery codes have been regenerated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the 2FA challenge screen (after login).
|
||||
*/
|
||||
public function challenge(Request $request)
|
||||
{
|
||||
if (!$request->session()->has('two_factor_user_id')) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
return Inertia::render('TwoFactor/Challenge');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the 2FA challenge code after login.
|
||||
*/
|
||||
public function verify(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'code' => 'required|string',
|
||||
]);
|
||||
|
||||
$userId = $request->session()->get('two_factor_user_id');
|
||||
|
||||
if (!$userId) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
$user = \App\Models\User::find($userId);
|
||||
|
||||
if (!$user || !$user->two_factor_secret) {
|
||||
$request->session()->forget('two_factor_user_id');
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
$code = $request->code;
|
||||
$valid = $this->google2fa->verifyKey($user->two_factor_secret, $code);
|
||||
|
||||
if (!$valid) {
|
||||
$recoveryCodes = json_decode($user->two_factor_recovery_codes ?? '[]', true);
|
||||
if (in_array($code, $recoveryCodes)) {
|
||||
$remaining = array_filter($recoveryCodes, fn($c) => $c !== $code);
|
||||
$user->update(['two_factor_recovery_codes' => json_encode(array_values($remaining))]);
|
||||
$valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$valid) {
|
||||
return back()->withErrors(['code' => 'Invalid code. Please try again.']);
|
||||
}
|
||||
|
||||
$request->session()->forget('two_factor_user_id');
|
||||
|
||||
\Illuminate\Support\Facades\Auth::login($user);
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exports\UsersExport;
|
||||
use App\Imports\UsersImport;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', User::class);
|
||||
|
||||
$trashed = $request->input('trashed');
|
||||
$search = $request->input('search');
|
||||
$status = $request->input('status');
|
||||
$role = $request->input('role');
|
||||
$sortField = $request->input('sort_field', 'created_at');
|
||||
$sortDir = $request->input('sort_direction', 'desc');
|
||||
$perPage = (int) $request->input('per_page', 15);
|
||||
|
||||
$query = User::with('roles');
|
||||
|
||||
if ($trashed === 'only') {
|
||||
$query->onlyTrashed();
|
||||
} elseif ($trashed === 'with') {
|
||||
$query->withTrashed();
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('first_name', 'like', "%{$search}%")
|
||||
->orWhere('last_name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($status) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
if ($role) {
|
||||
$query->role($role);
|
||||
}
|
||||
|
||||
$allowedSortFields = ['first_name', 'last_name', 'email', 'status', 'created_at'];
|
||||
if (!\in_array($sortField, $allowedSortFields, true)) {
|
||||
$sortField = 'created_at';
|
||||
}
|
||||
$sortDir = $sortDir === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$users = $query->orderBy($sortField, $sortDir)
|
||||
->paginate($perPage)
|
||||
->withQueryString();
|
||||
|
||||
$roles = Role::where('guard_name', 'web')->pluck('name');
|
||||
|
||||
return Inertia::render('Users/Index', [
|
||||
'users' => [
|
||||
'data' => $users->items(),
|
||||
'meta' => [
|
||||
'current_page' => $users->currentPage(),
|
||||
'last_page' => $users->lastPage(),
|
||||
'total' => $users->total(),
|
||||
'per_page' => $users->perPage(),
|
||||
],
|
||||
'links' => $users->linkCollection()->toArray(),
|
||||
],
|
||||
'filters' => $request->only(['search', 'status', 'role', 'sort_field', 'sort_direction', 'per_page', 'trashed']),
|
||||
'availableRoles' => $roles,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(User $user)
|
||||
{
|
||||
$this->authorize('view', $user);
|
||||
$user->load(['roles', 'permissions']);
|
||||
|
||||
return Inertia::render('Users/Show', [
|
||||
'viewUser' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
|
||||
$validated = $request->validate([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users',
|
||||
'password' => ['required', Password::defaults()],
|
||||
'status' => 'in:active,inactive',
|
||||
'roles' => 'nullable|array',
|
||||
'roles.*' => 'string|exists:roles,name',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'first_name' => $validated['first_name'],
|
||||
'last_name' => $validated['last_name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'status' => $validated['status'] ?? 'active',
|
||||
]);
|
||||
|
||||
if (!empty($validated['roles'])) {
|
||||
$user->syncRoles($validated['roles']);
|
||||
}
|
||||
|
||||
return back()->with('success', 'User created successfully.');
|
||||
}
|
||||
|
||||
public function update(Request $request, User $user)
|
||||
{
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$validated = $request->validate([
|
||||
'first_name' => 'sometimes|string|max:255',
|
||||
'last_name' => 'sometimes|string|max:255',
|
||||
'email' => 'sometimes|email|unique:users,email,' . $user->id,
|
||||
'status' => 'sometimes|in:active,inactive',
|
||||
'roles' => 'nullable|array',
|
||||
'roles.*' => 'string|exists:roles,name',
|
||||
]);
|
||||
|
||||
$roles = $validated['roles'] ?? null;
|
||||
unset($validated['roles']);
|
||||
|
||||
$user->update($validated);
|
||||
|
||||
if ($roles !== null) {
|
||||
$user->syncRoles($roles);
|
||||
}
|
||||
|
||||
return back()->with('success', 'User updated successfully.');
|
||||
}
|
||||
|
||||
public function destroy(User $user)
|
||||
{
|
||||
$this->authorize('delete', $user);
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return back()->withErrors(['error' => 'You cannot delete your own account.']);
|
||||
}
|
||||
|
||||
$user->delete();
|
||||
|
||||
return back()->with('success', 'Entity moved to archive.');
|
||||
}
|
||||
|
||||
public function restore(int $id)
|
||||
{
|
||||
$user = User::withTrashed()->findOrFail($id);
|
||||
$this->authorize('restore', $user);
|
||||
|
||||
$user->restore();
|
||||
|
||||
return back()->with('success', 'Entity restored from archive.');
|
||||
}
|
||||
|
||||
public function forceDelete(int $id)
|
||||
{
|
||||
$user = User::withTrashed()->findOrFail($id);
|
||||
$this->authorize('forceDelete', $user);
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return back()->withErrors(['error' => 'You cannot delete your own account.']);
|
||||
}
|
||||
|
||||
$user->forceDelete();
|
||||
|
||||
return back()->with('success', 'Entity permanently purged.');
|
||||
}
|
||||
|
||||
public function bulkArchive(Request $request)
|
||||
{
|
||||
$this->authorize('user.delete');
|
||||
|
||||
$ids = array_filter(
|
||||
(array) $request->input('ids', []),
|
||||
fn ($id) => (int) $id !== auth()->id()
|
||||
);
|
||||
|
||||
User::whereIn('id', $ids)->delete();
|
||||
|
||||
return back()->with('success', \count($ids) . ' users archived.');
|
||||
}
|
||||
|
||||
public function bulkRestore(Request $request)
|
||||
{
|
||||
$this->authorize('user.delete');
|
||||
|
||||
$ids = (array) $request->input('ids', []);
|
||||
|
||||
User::withTrashed()->whereIn('id', $ids)->restore();
|
||||
|
||||
return back()->with('success', \count($ids) . ' users restored.');
|
||||
}
|
||||
|
||||
public function bulkForceDelete(Request $request)
|
||||
{
|
||||
$this->authorize('user.delete');
|
||||
|
||||
$ids = array_filter(
|
||||
(array) $request->input('ids', []),
|
||||
fn ($id) => (int) $id !== auth()->id()
|
||||
);
|
||||
|
||||
User::withTrashed()->whereIn('id', $ids)->forceDelete();
|
||||
|
||||
return back()->with('success', \count($ids) . ' users permanently deleted.');
|
||||
}
|
||||
|
||||
public function export()
|
||||
{
|
||||
$this->authorize('viewAny', User::class);
|
||||
|
||||
return Excel::download(new UsersExport, 'users-' . now()->format('Y-m-d') . '.xlsx');
|
||||
}
|
||||
|
||||
public function import(Request $request)
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
|
||||
$request->validate([
|
||||
'file' => 'required|mimes:xlsx,csv,xls|max:5120',
|
||||
]);
|
||||
|
||||
Excel::import(new UsersImport, $request->file('file'));
|
||||
|
||||
return back()->with('success', 'Users imported successfully.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that is loaded on the first page visit.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determine the current asset version.
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
$settings = \Illuminate\Support\Facades\Cache::rememberForever('system_settings', function() {
|
||||
return \App\Models\Setting::pluck('value', 'key')->toArray();
|
||||
});
|
||||
|
||||
// Inject default defaults if not in DB
|
||||
$defaultSettings = [
|
||||
'app_name' => 'biiskit',
|
||||
'app_logo' => null,
|
||||
'app_logo_text' => 'B',
|
||||
'allow_registration' => '1',
|
||||
];
|
||||
$settings = array_merge($defaultSettings, $settings);
|
||||
|
||||
return [
|
||||
...parent::share($request),
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'permissions' => $request->user()?->getAllPermissions()->pluck('name') ?? [],
|
||||
'roles' => $request->user()?->getRoleNames() ?? [],
|
||||
],
|
||||
'system_settings' => $settings,
|
||||
'unread_notifications' => $request->user()
|
||||
? \App\Models\NotificationLog::where('status', 'sent')
|
||||
->where('created_at', '>=', now()->subDays(7))
|
||||
->count()
|
||||
: 0,
|
||||
'flash' => [
|
||||
'success' => $request->session()->get('success'),
|
||||
'error' => $request->session()->get('error'),
|
||||
'plain_text_token' => $request->session()->get('plain_text_token'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => ['required', 'string', 'max:255'],
|
||||
'last_name' => ['required', 'string', 'max:255'],
|
||||
'phone' => ['nullable', 'string', 'max:20'],
|
||||
'bio' => ['nullable', 'string', 'max:1000'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'firstName' => $this->first_name,
|
||||
'lastName' => $this->last_name,
|
||||
'fullName' => $this->getFullName(),
|
||||
'email' => $this->email,
|
||||
'phone' => $this->phone,
|
||||
'bio' => $this->bio,
|
||||
'status' => $this->status,
|
||||
'avatarUrl' => $this->avatar_url,
|
||||
'roles' => $this->getRoleNames(),
|
||||
'permissions' => $this->getAllPermissions()->pluck('name'),
|
||||
'createdAt' => $this->created_at,
|
||||
'updatedAt' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user