diff --git a/app/Actions/Accounts/CreateAccountAction.php b/app/Actions/Accounts/CreateAccountAction.php new file mode 100644 index 0000000..83de647 --- /dev/null +++ b/app/Actions/Accounts/CreateAccountAction.php @@ -0,0 +1,20 @@ +email); + } + } +} diff --git a/app/Http/Controllers/AccountsController.php b/app/Http/Controllers/AccountsController.php new file mode 100644 index 0000000..08d8fbf --- /dev/null +++ b/app/Http/Controllers/AccountsController.php @@ -0,0 +1,50 @@ +execute($request->validated()); + + return redirect()->route('accounts.index'); + } + + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, string $id) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy($account) + { + User::findOrFail($account)->delete(); + + return redirect()->route('accounts.index'); + } +} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php deleted file mode 100644 index 53a546b..0000000 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ /dev/null @@ -1,51 +0,0 @@ -validate([ - 'name' => 'required|string|max:255', - 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - $user = User::create([ - 'name' => $request->name, - 'email' => $request->email, - 'password' => Hash::make($request->password), - ]); - - event(new Registered($user)); - - Auth::login($user); - - return redirect(route('dashboard', absolute: false)); - } -} diff --git a/app/Http/Requests/CreateAccountRequest.php b/app/Http/Requests/CreateAccountRequest.php new file mode 100644 index 0000000..c44a746 --- /dev/null +++ b/app/Http/Requests/CreateAccountRequest.php @@ -0,0 +1,37 @@ +user()->role == 'admin'; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'username' => 'required|regex:/^[a-zA-Z0-9_-]+$/|string|max:25|unique:' . User::class, + 'email' => 'required|string|lowercase|email|max:255|unique:' . User::class, + 'password' => ['required', Password::defaults()], + 'role' => ['required', 'string', 'in:admin,user'], + 'domain_limit' => ['nullable', 'integer', 'min:1'], + 'database_limit' => ['nullable', 'integer', 'min:1'], + 'notify' => ['nullable', 'boolean'], + ]; + } +} diff --git a/app/Listeners/MessageReceivedListener.php b/app/Listeners/MessageReceivedListener.php index c4dec61..9eca7ab 100644 --- a/app/Listeners/MessageReceivedListener.php +++ b/app/Listeners/MessageReceivedListener.php @@ -4,7 +4,6 @@ namespace App\Listeners; use App\Events\SystemStatsEvent; use App\Events\TopStatsEvent; -use Illuminate\Support\Facades\Log; use Laravel\Reverb\Events\MessageReceived; class MessageReceivedListener @@ -15,13 +14,10 @@ class MessageReceivedListener */ public function handle(MessageReceived $event) { - Log::info('MessageReceivedListener::handle called'); $msg = json_decode($event->message); if ($msg->event == 'client-typing') { - Log::info('Received client-typing'); - match ($msg->channel) { 'private-systemstats' => (new SystemStatsEvent())->dispatch(), 'private-topstats' => (new TopStatsEvent())->dispatch(), diff --git a/app/Models/User.php b/app/Models/User.php index e226a94..c7ce43f 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -19,8 +19,12 @@ class User extends Authenticatable */ protected $fillable = [ 'name', + 'username', 'email', 'password', + 'role', + 'domain_limit', + 'database_limit', ]; /** diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index a702529..47822d3 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -15,9 +15,12 @@ return new class extends Migration $table->id(); $table->string('name'); $table->string('email')->unique(); + $table->string('username')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->enum('role', ['admin', 'user'])->default('user'); + $table->integer('domain_limit')->nullable(); + $table->integer('database_limit')->nullable(); $table->rememberToken(); $table->timestamps(); }); diff --git a/resources/js/Components/ConfirmationButton.jsx b/resources/js/Components/ConfirmationButton.jsx new file mode 100644 index 0000000..1d29426 --- /dev/null +++ b/resources/js/Components/ConfirmationButton.jsx @@ -0,0 +1,47 @@ +import Modal from '@/Components/Modal'; +import { useState } from 'react'; +import DangerButton from './DangerButton'; +import SecondaryButton from './SecondaryButton'; + +export default function ConfirmationButton({ children, buttonClassName = '', doAction }) { + + const [showModal, setShowModal] = useState(false); + + const closeModal = () => { + setShowModal(false); + }; + + return (<> + + +
+

+ Are you sure? +

+ +
+

+ This action cannot be undone. +

+
+ +
+ { + doAction(); + closeModal(); + }} className='mr-2'> + Yes, I'm sure + + + closeModal()}> + Cancel + +
+ +
+
+ + ); +} diff --git a/resources/js/Components/InputRadio.jsx b/resources/js/Components/InputRadio.jsx new file mode 100644 index 0000000..450ef7c --- /dev/null +++ b/resources/js/Components/InputRadio.jsx @@ -0,0 +1,12 @@ +export default function InputRadio({ className = '', ...props }) { + return ( + + ); +} diff --git a/resources/js/Layouts/AuthenticatedLayout.jsx b/resources/js/Layouts/AuthenticatedLayout.jsx index 8c242db..a1c10c4 100644 --- a/resources/js/Layouts/AuthenticatedLayout.jsx +++ b/resources/js/Layouts/AuthenticatedLayout.jsx @@ -10,6 +10,7 @@ export default function AuthenticatedLayout({ header, children }) { return (
+ diff --git a/resources/js/Layouts/Partials/SidebarNavi.jsx b/resources/js/Layouts/Partials/SidebarNavi.jsx index 16ec985..2b40335 100644 --- a/resources/js/Layouts/Partials/SidebarNavi.jsx +++ b/resources/js/Layouts/Partials/SidebarNavi.jsx @@ -1,10 +1,16 @@ -import { Link } from "@inertiajs/react"; +import { Link, usePage } from "@inertiajs/react"; import { RiDashboard3Fill } from "react-icons/ri"; import { ImProfile } from "react-icons/im"; -import { FaUsers } from "react-icons/fa6"; +import { FaPhp, FaUsers } from "react-icons/fa6"; import { VscFileSubmodule } from "react-icons/vsc"; +import { TbBrandMysql } from "react-icons/tb"; +import { MdSecurity } from "react-icons/md"; +import { IoLockClosedOutline } from "react-icons/io5"; const SidebarNavi = () => { + + const { auth } = usePage().props; + return (
    @@ -19,7 +25,7 @@ const SidebarNavi = () => {
  • @@ -29,6 +35,35 @@ const SidebarNavi = () => {
  • + {auth.user.role == 'admin' && ( +
  • + +
    + +
    + Accounts + +
  • + )} + + + {auth.user.role == 'admin' && ( +
  • + +
    + +
    + Firewall + +
  • + )} +
  • {
  • - +
    - Accounts + MySQL DBs + +
  • + +
  • + +
    + +
    + PHP Manager + +
  • + +
  • + +
    + +
    + SSL Manager
  • diff --git a/resources/js/Pages/Accounts/Index.jsx b/resources/js/Pages/Accounts/Index.jsx new file mode 100644 index 0000000..97326a3 --- /dev/null +++ b/resources/js/Pages/Accounts/Index.jsx @@ -0,0 +1,88 @@ +import { FaUsers } from "react-icons/fa6"; +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import { Head, Link } from '@inertiajs/react'; +import CreateUserForm from './Partials/CreateAccountForm'; +import ConfirmationButton from "@/Components/ConfirmationButton"; +import { TiDelete } from "react-icons/ti"; +import { toast } from "react-toastify"; +import { router } from '@inertiajs/react' + +export default function Accounts({ accounts }) { + + const deleteUser = (id) => { + router.delete(route('accounts.destroy', { account: id }), { + onSuccess: page => { + toast("Account deleted successfully."); + }, + onError: errors => { + toast("Error occured while deleting account."); + console.log(errors); + }, + }); + }; + + return ( + +

    + + Accounts +

    + +
+ } + > + + +
+ +
+ + + + + + + + + + + + + {accounts?.map((account, index) => ( + + + + + + + + + ))} + +
IDNameEmailLimitsRoleActions
+ {account.id} + + {account.name} + + {account.email} + + {account.limits?.db}/{account.limits?.domains} + + {account.role == "admin" ? Admin : User} + + Edit / Impersonate + + deleteUser(account.id)}> + + + +
+
+
+ + ); +} + diff --git a/resources/js/Pages/Accounts/Partials/CreateAccountForm.jsx b/resources/js/Pages/Accounts/Partials/CreateAccountForm.jsx new file mode 100644 index 0000000..34bf28b --- /dev/null +++ b/resources/js/Pages/Accounts/Partials/CreateAccountForm.jsx @@ -0,0 +1,311 @@ +import DangerButton from '@/Components/DangerButton'; +import InputError from '@/Components/InputError'; +import InputLabel from '@/Components/InputLabel'; +import Modal from '@/Components/Modal'; +import PrimaryButton from '@/Components/PrimaryButton'; +import SecondaryButton from '@/Components/SecondaryButton'; +import Checkbox from '@/Components/Checkbox'; +import TextInput from '@/Components/TextInput'; +import { useForm } from '@inertiajs/react'; +import { useRef, useState } from 'react'; +import { FaUserPlus } from 'react-icons/fa6'; +import InputRadio from '@/Components/InputRadio'; +import { toast } from 'react-toastify'; + +export default function CreateAccountForm() { + const [showModal, setShowModal] = useState(false); + + const randomPassword = () => { + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + let result = ''; + for (let i = 0; i < 12; i++) { + result += characters.charAt( + Math.floor(Math.random() * charactersLength) + ); + } + return result; + }; + + const { + data, + setData, + post, + processing, + reset, + errors, + clearErrors, + } = useForm({ + username: '', + name: '', + email: '', + password: randomPassword(), + role: '', + domain_limit: null, + database_limit: null, + notify: false, + }); + + const showCreateModal = () => { + setShowModal(true); + }; + + const createUser = (e) => { + e.preventDefault(); + + post(route('accounts.store'), { + preserveScroll: true, + onSuccess: () => { + closeModal(); + reset(); + toast("Account created successfully."); + } + }); + }; + + const closeModal = () => { + setShowModal(false); + + clearErrors(); + reset(); + }; + + return ( + <> + + + +
+

+ + Create a New Account +

+ +
+ + + + + + setData('name', e.target.value) + } + className="mt-1 block w-full" + isFocused + placeholder="Full Name" + /> + + + + + + + + setData('username', e.target.value) + } + className="mt-1 block w-full" + placeholder="Username" + /> + + + + + + + setData('email', e.target.value) + } + className="mt-1 block w-full" + placeholder="Email" + /> + + + + + + + setData('password', e.target.value) + } + className="mt-1 block w-full" + placeholder="Password" + /> + + + + + +
+ + setData('role', e.target.checked ? 'admin' : 'user') + } + className="h-4 w-4" + value="admin" + /> + + Admin + + + + setData('role', e.target.checked ? 'user' : 'admin') + } + className="h-4 w-4" + /> + + User + +
+ + + +
+ +
+ + + + setData('domain_limit', e.target.value) + } + className="mt-1 block w-full" + placeholder="Domain Limit" + /> + + +
+ +
+ + + + setData('database_limit', e.target.value) + } + className="mt-1 block w-full" + placeholder="Database Limit" + /> + + +
+ +
+ +
+ + setData('notify', e.target.checked) + } + className="mr-1" + /> +
+ Notify user with account logins via email +
+
+ + + +
+ + Create Account + + + + Cancel + +
+ +
+
+
+ + ); +} diff --git a/resources/js/Pages/Filemanager/Filemanager.jsx b/resources/js/Pages/Filemanager/Filemanager.jsx index bd57e53..bd74985 100644 --- a/resources/js/Pages/Filemanager/Filemanager.jsx +++ b/resources/js/Pages/Filemanager/Filemanager.jsx @@ -163,7 +163,6 @@ const Filemanager = () => { Filemanager -
} > @@ -187,7 +186,6 @@ const Filemanager = () => { Filemanager -
} > diff --git a/resources/js/Pages/Stats/History.jsx b/resources/js/Pages/Stats/History.jsx index d96dbb1..0e212d4 100644 --- a/resources/js/Pages/Stats/History.jsx +++ b/resources/js/Pages/Stats/History.jsx @@ -52,7 +52,6 @@ export default function StatsHistory({ selectedDate, cpuStats, memoryStats, netw } > -
diff --git a/routes/auth.php b/routes/auth.php index 3926ecf..eba74db 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -12,11 +12,6 @@ use App\Http\Controllers\Auth\VerifyEmailController; use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { - Route::get('register', [RegisteredUserController::class, 'create']) - ->name('register'); - - Route::post('register', [RegisteredUserController::class, 'store']); - Route::get('login', [AuthenticatedSessionController::class, 'create']) ->name('login'); diff --git a/routes/web.php b/routes/web.php index a156273..7361b1e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ middleware(['auth', AdminMiddleware::class])->name('dashboard.admin.setTopSort'); Route::get('/dashboard/user', [DashboardController::class, 'user'])->middleware(['auth'])->name('dashboard.user'); -// Filemanager + +// Accounts [Admin] +Route::resource('/accounts', AccountsController::class)->middleware(['auth', AdminMiddleware::class])->except(['create', 'edit', 'show']); + +// Filemanager [Admin | User] Route::get('/filemanager', [FilemanagerController::class, 'index'])->middleware(['auth'])->name('filemanager'); Route::get('/filemanager/get-directory-contents', [FilemanagerController::class, 'getDirectoryContents'])->middleware(['auth'])->name('filemanager.getDirectorContents'); Route::get('/filemanager/get-file-contents', [FilemanagerController::class, 'getFileContents'])->middleware(['auth'])->name('filemanager.getFileContents'); @@ -31,7 +36,7 @@ Route::patch('/filemanager/paste-files', [FilemanagerController::class, 'pasteFi Route::post('/filemanager/delete-files', [FilemanagerController::class, 'deleteFiles'])->middleware(['auth'])->name('filemanager.deleteFiles'); Route::post('/filemanager/upload-file', [FilemanagerController::class, 'uploadFile'])->middleware(['auth'])->name('filemanager.uploadFile'); -// Stats History +// Stats History [Admin] Route::get('/stats/history', [StatsHistoryController::class, 'cpuAndMemory'])->middleware(['auth', AdminMiddleware::class])->name('stats.history'); // Accounts