mirror of
https://github.com/crivion/laranode.git
synced 2026-05-06 13:41:02 +08:00
Websites adding in progress
This commit is contained in:
75
app/Http/Controllers/WebsiteController.php
Normal file
75
app/Http/Controllers/WebsiteController.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Website;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class WebsiteController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): \Inertia\Response
|
||||
{
|
||||
$websites = Website::mine()->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)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
31
app/Models/Website.php
Normal file
31
app/Models/Website.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Website extends Model
|
||||
{
|
||||
|
||||
protected $fillable = [
|
||||
'url',
|
||||
'document_root',
|
||||
'php_version_id',
|
||||
];
|
||||
|
||||
public function scopeMine(Builder $query): Builder
|
||||
{
|
||||
return $query->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('websites', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<input
|
||||
{...props}
|
||||
type={type}
|
||||
className={
|
||||
'rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600 ' +
|
||||
className
|
||||
}
|
||||
ref={localRef}
|
||||
/>
|
||||
<div className="flex rounded-md">
|
||||
{prependedText && (
|
||||
<span className="text-xs inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-100 text-gray-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400">
|
||||
{prependedText}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
{...props}
|
||||
type={type}
|
||||
className={
|
||||
`${prependedText ? 'rounded-r-md' : 'rounded-md'} flex-1 border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600 ${className}`
|
||||
}
|
||||
ref={localRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -59,13 +59,13 @@ const SidebarNavi = () => {
|
||||
|
||||
<li>
|
||||
<Link
|
||||
href="/domains"
|
||||
href={route('websites.index')}
|
||||
className="relative flex flex-row items-center h-11 focus:outline-none hover:bg-gray-900 text-gray-300 border-l-4 border-transparent hover:border-indigo-900 pr-6"
|
||||
>
|
||||
<div>
|
||||
<TbWorldWww className="ml-3 w-5 h-5" />
|
||||
</div>
|
||||
<span className="ml-2 text-sm tracking-wide truncate">Domains</span>
|
||||
<span className="ml-2 text-sm tracking-wide truncate">Websites</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
|
||||
104
resources/js/Pages/Websites/Index.jsx
Normal file
104
resources/js/Pages/Websites/Index.jsx
Normal file
@@ -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 (
|
||||
<AuthenticatedLayout
|
||||
header={
|
||||
<div className="flex flex-col xl:flex-row xl:justify-between max-w-7xl pr-5">
|
||||
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight flex items-center">
|
||||
<TbWorldWww className='mr-2' />
|
||||
Websites
|
||||
</h2>
|
||||
<CreateWebsiteForm serverIp={serverIp} className="max-w-xl" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Head title="Websites" />
|
||||
|
||||
<div className="max-w-7xl px-4 my-8">
|
||||
|
||||
<div className="relative overflow-x-auto bg-white dark:bg-gray-850 mt-3">
|
||||
<table className="w-full text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead className="text-gray-700 uppercase bg-gray-200 dark:bg-gray-700 dark:text-gray-300 text-sm">
|
||||
<tr>
|
||||
<th className="px-6 py-3">ID</th>
|
||||
<th className="px-6 py-3">URL</th>
|
||||
<th className="px-6 py-3">Document Root</th>
|
||||
<th className="px-6 py-3">PHP Version</th>
|
||||
{auth.user.role == 'admin' && (
|
||||
<th className="px-6 py-3">User</th>
|
||||
)}
|
||||
<th className="px-6 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
{websites?.map((website, index) => (
|
||||
<tr key={`website-${index}`} className="bg-white border-b text-gray-700 dark:text-gray-200 dark:bg-gray-850 dark:border-gray-700 border-gray-200">
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{website.url}
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{website.document_root}
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{website.php_version.version}
|
||||
</td>
|
||||
{auth.user.role == 'admin' && (
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{website.user.username}
|
||||
<div>
|
||||
{website.user.username}
|
||||
</div>
|
||||
{website.user.role == "admin" ? <span className='bg-green-300 text-green-700 px-2 py-1 text-sm rounded-lg'>Admin</span> : <span className='bg-gray-300 text-gray-700 px-2 py-1 text-sm rounded-lg'>User</span>}
|
||||
</td>
|
||||
)}
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{account.role == "admin" ? <span className='bg-green-300 text-green-700 px-2 py-1 text-sm rounded-lg'>Admin</span> : <span className='bg-gray-300 text-gray-700 px-2 py-1 text-sm rounded-lg'>User</span>}
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900
|
||||
whitespace-nowrap dark:text-white">
|
||||
|
||||
<div className='flex items-center space-x-2'>
|
||||
{/* <EditAccountForm account={account} /> */}
|
||||
|
||||
<ConfirmationButton doAction={() => deleteWebsite(website.id)}>
|
||||
<TiDelete className='w-6 h-6 text-red-500' />
|
||||
</ConfirmationButton>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
|
||||
165
resources/js/Pages/Websites/Partials/CreateWebsiteForm.jsx
Normal file
165
resources/js/Pages/Websites/Partials/CreateWebsiteForm.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<button onClick={showCreateModal} className='flex items-center text-gray-700 dark:text-gray-300'>
|
||||
<TbWorldWww className='mr-2' />
|
||||
Create Website
|
||||
</button>
|
||||
|
||||
<Modal show={showModal} onClose={closeModal}>
|
||||
<form onSubmit={createWebsite} className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<TbWorldWww className='mr-2' />
|
||||
Add a New Website
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 flex flex-col space-y-4 max-h-[500px] overflow-scroll">
|
||||
|
||||
<div class="bg-gray-200 dark:bg-gray-700 p-4 rounded-md text-gray-700 dark:text-gray-300 flex items-center text-xs">
|
||||
<div>
|
||||
<BsFillInfoCircleFill className='mr-2 h-6 w-6' />
|
||||
</div>
|
||||
<div>
|
||||
IMPORTANT: You must point your domain A record via DNS to this server IP:
|
||||
<br />
|
||||
<CopyToClipboard onCopy={() => setIpCopied(true)} text={serverIp}>
|
||||
<span className="cursor-pointer">
|
||||
{serverIp}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
|
||||
<Transition
|
||||
show={ipCopied}
|
||||
enter="transition ease-in-out"
|
||||
enterFrom="opacity-0"
|
||||
leave="transition ease-in-out"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-xs">
|
||||
IP copied to clipboard.
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel
|
||||
htmlFor="url"
|
||||
value="URL *no protocol [http|https] - just the domain"
|
||||
className='my-2'
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="url"
|
||||
name="url"
|
||||
value={data.url}
|
||||
onChange={(e) =>
|
||||
setData('url', e.target.value)
|
||||
}
|
||||
className="mt-1 block w-full"
|
||||
isFocused
|
||||
placeholder="example.org"
|
||||
required
|
||||
/>
|
||||
|
||||
<InputError
|
||||
message={errors.url}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel htmlFor="document_root" className='my-2'>
|
||||
<div className="block text-sm font-medium text-gray-700 dark:text-gray-300 my-2">
|
||||
Document Root
|
||||
</div>
|
||||
</InputLabel>
|
||||
|
||||
<TextInput
|
||||
id="document_root"
|
||||
name="document_root"
|
||||
value={data.document_root}
|
||||
onChange={(e) => setData('document_root', e.target.value)}
|
||||
className="mt-1 block w-full"
|
||||
placeholder="Document Root"
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="text-xs inline-flex items-center px-3 rounded-md border border-gray-300 bg-gray-100 text-gray-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400">
|
||||
{auth.user.homedir}/domains/{data.url}{data.document_root}
|
||||
</div>
|
||||
|
||||
<InputError
|
||||
message={errors.document_root}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<PrimaryButton className="mr-3" disabled={processing}>
|
||||
Add Website
|
||||
</PrimaryButton>
|
||||
|
||||
<SecondaryButton onClick={closeModal}>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</form >
|
||||
</Modal >
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user