From 2a56c1bb158be92132a5ac8a8e38dfc2e0f1d697 Mon Sep 17 00:00:00 2001 From: Alex Crivion Date: Tue, 25 Feb 2025 12:10:13 +0000 Subject: [PATCH] Websites adding in progress --- app/Http/Controllers/WebsiteController.php | 75 ++++++++ app/Models/User.php | 7 + app/Models/Website.php | 31 ++++ ...025_02_25_062355_create_websites_table.php | 31 ++++ package.json | 1 + resources/js/Components/TextInput.jsx | 32 ++-- resources/js/Layouts/Partials/SidebarNavi.jsx | 4 +- resources/js/Pages/Websites/Index.jsx | 104 +++++++++++ .../Websites/Partials/CreateWebsiteForm.jsx | 165 ++++++++++++++++++ routes/web.php | 6 +- 10 files changed, 442 insertions(+), 14 deletions(-) create mode 100644 app/Http/Controllers/WebsiteController.php create mode 100644 app/Models/Website.php create mode 100644 database/migrations/2025_02_25_062355_create_websites_table.php create mode 100644 resources/js/Pages/Websites/Index.jsx create mode 100644 resources/js/Pages/Websites/Partials/CreateWebsiteForm.jsx diff --git a/app/Http/Controllers/WebsiteController.php b/app/Http/Controllers/WebsiteController.php new file mode 100644 index 0000000..1c7c5e1 --- /dev/null +++ b/app/Http/Controllers/WebsiteController.php @@ -0,0 +1,75 @@ +with(['user', 'phpVersion'])->orderBy('url')->get(); + + try { + $serverIp = Http::get('https://api.ipify.org')->body(); + } catch (\Exception $exception) { + $serverIp = 'N/A'; + } + + return Inertia::render('Websites/Index', compact('websites', 'serverIp')); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + // + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + // + } + + /** + * Display the specified resource. + */ + public function show(string $id) + { + // + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(string $id) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, string $id) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id) + { + // + } +} diff --git a/app/Models/User.php b/app/Models/User.php index a4d4c84..c526057 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -15,6 +15,8 @@ class User extends Authenticatable /** @use HasFactory<\Database\Factories\UserFactory> */ use HasFactory, Notifiable, Impersonate; + public $appends = ['homedir']; + /** * The attributes that are mass assignable. * @@ -87,4 +89,9 @@ class User extends Authenticatable get: fn() => $this->username . '_ln', ); } + + public function websites(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Website::class); + } } diff --git a/app/Models/Website.php b/app/Models/Website.php new file mode 100644 index 0000000..d8c8d5f --- /dev/null +++ b/app/Models/Website.php @@ -0,0 +1,31 @@ +when(!auth()->user()->isAdmin(), fn($query) => $query->where('user_id', auth()->id())); + } + + public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class)->select(['id', 'username', 'role']); + } + + public function phpVersion(): \Illuminate\Database\Eloquent\Relations\HasOne + { + return $this->hasOne(PhpVersion::class); + } +} diff --git a/database/migrations/2025_02_25_062355_create_websites_table.php b/database/migrations/2025_02_25_062355_create_websites_table.php new file mode 100644 index 0000000..b5ffd96 --- /dev/null +++ b/database/migrations/2025_02_25_062355_create_websites_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('user_id'); + $table->string('url'); + $table->string('document_root'); + $table->foreignId('php_version_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('websites'); + } +}; diff --git a/package.json b/package.json index 6496aa4..4f74a10 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "chart.js": "^4.4.7", "prismjs": "^1.29.0", "react-chartjs-2": "^5.3.0", + "react-copy-to-clipboard": "^5.1.0", "react-data-table-component": "^7.6.2", "react-file-icon": "^1.5.0", "react-icons": "^5.4.0", diff --git a/resources/js/Components/TextInput.jsx b/resources/js/Components/TextInput.jsx index d583c47..b8a9341 100644 --- a/resources/js/Components/TextInput.jsx +++ b/resources/js/Components/TextInput.jsx @@ -1,7 +1,13 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; export default forwardRef(function TextInput( - { type = 'text', className = '', isFocused = false, ...props }, + { + type = 'text', + className = '', + isFocused = false, + prependedText = '', + ...props + }, ref, ) { const localRef = useRef(null); @@ -17,14 +23,20 @@ export default forwardRef(function TextInput( }, [isFocused]); return ( - +
+ {prependedText && ( + + {prependedText} + + )} + +
); }); diff --git a/resources/js/Layouts/Partials/SidebarNavi.jsx b/resources/js/Layouts/Partials/SidebarNavi.jsx index 798f527..4e844e6 100644 --- a/resources/js/Layouts/Partials/SidebarNavi.jsx +++ b/resources/js/Layouts/Partials/SidebarNavi.jsx @@ -59,13 +59,13 @@ const SidebarNavi = () => {
  • - Domains + Websites
  • diff --git a/resources/js/Pages/Websites/Index.jsx b/resources/js/Pages/Websites/Index.jsx new file mode 100644 index 0000000..a867b61 --- /dev/null +++ b/resources/js/Pages/Websites/Index.jsx @@ -0,0 +1,104 @@ +import { FaUsers } from "react-icons/fa6"; +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import { Head, Link, usePage } from '@inertiajs/react'; +import ConfirmationButton from "@/Components/ConfirmationButton"; +import { TiDelete } from "react-icons/ti"; +import { toast } from "react-toastify"; +import { router } from '@inertiajs/react' +import { TbWorldWww } from "react-icons/tb"; +import { FaDatabase, FaEdit } from "react-icons/fa"; +import { Tooltip } from 'react-tooltip' +import CreateWebsiteForm from "./Partials/CreateWebsiteForm"; + +export default function Websites({ websites, serverIp }) { + + const { auth } = usePage().props; + + const deleteWebsite = (id) => { + router.delete(route('accounts.destroy', { account: id }), { + onBefore: () => { + toast("Please wait, deleting account and its resources..."); + }, + onError: errors => { + toast("Error occured while deleting account."); + console.log(errors); + }, + }); + }; + + return ( + +

    + + Websites +

    + + + } + > + + +
    + +
    + + + + + + + + {auth.user.role == 'admin' && ( + + )} + + + + + {websites?.map((website, index) => ( + + + + + {auth.user.role == 'admin' && ( + + )} + + + + ))} + +
    IDURLDocument RootPHP VersionUserActions
    + {website.url} + + {website.document_root} + + {website.php_version.version} + + {website.user.username} +
    + {website.user.username} +
    + {website.user.role == "admin" ? Admin : User} +
    + {account.role == "admin" ? Admin : User} + + +
    + {/* */} + + deleteWebsite(website.id)}> + + +
    + +
    +
    +
    +
    + ); +} + diff --git a/resources/js/Pages/Websites/Partials/CreateWebsiteForm.jsx b/resources/js/Pages/Websites/Partials/CreateWebsiteForm.jsx new file mode 100644 index 0000000..6be8f46 --- /dev/null +++ b/resources/js/Pages/Websites/Partials/CreateWebsiteForm.jsx @@ -0,0 +1,165 @@ +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 TextInput from '@/Components/TextInput'; +import { useForm, usePage } from '@inertiajs/react'; +import { useState } from 'react'; +import { BsFillInfoCircleFill } from "react-icons/bs"; +import { TbWorldWww } from 'react-icons/tb'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { Transition } from '@headlessui/react'; + +export default function CreateWebsiteForm({ serverIp }) { + const { auth } = usePage().props; + const [showModal, setShowModal] = useState(false); + const [ipCopied, setIpCopied] = useState(false); + + const { + data, + setData, + post, + processing, + reset, + errors, + clearErrors, + } = useForm({ + url: '', + document_root: '/', + php_version_id: null, + }); + + const showCreateModal = () => { + setShowModal(true); + }; + + const createWebsite = (e) => { + e.preventDefault(); + + post(route('websites.store'), { + preserveScroll: true, + onSuccess: () => { + closeModal(); + }, + }); + }; + + const closeModal = () => { + setShowModal(false); + + clearErrors(); + reset(); + }; + + return ( + <> + + + +
    +

    + + Add a New Website +

    + +
    + +
    +
    + +
    +
    + IMPORTANT: You must point your domain A record via DNS to this server IP: +
    + setIpCopied(true)} text={serverIp}> + + {serverIp} + + + + +

    + IP copied to clipboard. +

    +
    +
    +
    + +
    + + + + setData('url', e.target.value) + } + className="mt-1 block w-full" + isFocused + placeholder="example.org" + required + /> + + +
    + +
    + +
    + Document Root +
    +
    + + setData('document_root', e.target.value)} + className="mt-1 block w-full" + placeholder="Document Root" + required + /> + +
    + {auth.user.homedir}/domains/{data.url}{data.document_root} +
    + + +
    + +
    + + Add Website + + + + Cancel + +
    +
    +
    +
    + + ); +} diff --git a/routes/web.php b/routes/web.php index 89a53a7..fe57e52 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,10 +5,9 @@ use App\Http\Controllers\DashboardController; use App\Http\Controllers\FilemanagerController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\StatsHistoryController; +use App\Http\Controllers\WebsiteController; use App\Http\Middleware\AdminMiddleware; -use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; -use Inertia\Inertia; Route::get('/', function () { return redirect('/dashboard'); @@ -27,6 +26,9 @@ Route::resource('/accounts', AccountsController::class)->middleware(['auth', Adm Route::get('/accounts/impersonate/{user}', [AccountsController::class, 'impersonate'])->middleware(['auth', AdminMiddleware::class])->name('accounts.impersonate'); Route::get('/accounts/leave-impersonation', [AccountsController::class, 'leaveImpersonation'])->middleware(['auth'])->name('accounts.leaveImpersonation'); +// Websites [Admin | User] +Route::resource('/websites', WebsiteController::class)->middleware(['auth']); + // 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');