Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/Actions/Site/CreateSite.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function create(Server $server, array $input): Site

DB::beginTransaction();
try {
$user = $input['user'] ?? $server->getSshUser();
$user = $input['user'];
$site = new Site([
'server_id' => $server->id,
'type' => $input['type'],
Expand Down Expand Up @@ -110,7 +110,7 @@ private function validate(Server $server, array $input): void
new DomainRule,
],
'user' => [
'nullable',
'required',
'regex:/^[a-z_][a-z0-9_-]*[a-z0-9]$/',
'min:3',
'max:32',
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/password-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(({
/>
<button
type="button"
className="absolute right-0 top-0 flex h-9 w-9 items-center justify-center text-muted-foreground hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
className="text-muted-foreground hover:text-foreground absolute top-0 right-0 flex h-9 w-9 items-center justify-center disabled:pointer-events-none disabled:opacity-50"
onClick={() => setShowPassword((prev) => !prev)}
disabled={props.disabled}
aria-label={showPassword ? 'Hide password' : 'Show password'}
Expand Down
39 changes: 34 additions & 5 deletions resources/js/pages/sites/components/create-site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { Form, FormField, FormFields } from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { LoaderCircle } from 'lucide-react';
import { LoaderCircle, HelpCircle } from 'lucide-react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useForm, usePage } from '@inertiajs/react';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import InputError from '@/components/ui/input-error';
Expand Down Expand Up @@ -281,19 +283,46 @@ export default function CreateSite({
))}
</FormField>

{page.props.configs.site.types[form.data.type].form?.map((config) => getFormField(config))}

<FormField>
<Label htmlFor="user">Isolated User (Optional)</Label>
<Label htmlFor="user" className="flex items-center gap-1">
Isolated User
<Dialog>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<button type="button" className="text-muted-foreground hover:text-foreground">
<HelpCircle className="h-4 w-4" />
</button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>Why?</TooltipContent>
</Tooltip>
</TooltipProvider>
<DialogContent>
<DialogHeader>
<DialogTitle>Why Isolated Users?</DialogTitle>
<DialogDescription>
Isolated users are mandatory to ensure security for your sites. If a site has security vulnerabilities and gets
compromised, the attacker cannot take full control of the server because the site runs under its own isolated user
with limited permissions.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</Label>
<Input
id="user"
type="text"
value={form.data.user}
onChange={(e) => form.setData('user', e.target.value)}
placeholder="Leave empty for using server's default user"
placeholder="e.g. mysite"
/>
<p className="text-muted-foreground text-xs">The isolated user for the site. Must be unique on the server.</p>
<InputError message={form.errors.user} />
</FormField>

{page.props.configs.site.types[form.data.type].form?.map((config) => getFormField(config))}
</>
)}
</FormFields>
Expand Down
4 changes: 2 additions & 2 deletions tests/Feature/API/SitesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ public function test_create_site(array $inputs): void
->assertJsonFragment([
'domain' => $inputs['domain'],
'aliases' => $inputs['aliases'] ?? [],
'user' => $inputs['user'] ?? $this->server->getSshUser(),
'path' => '/home/'.($inputs['user'] ?? $this->server->getSshUser()).'/'.$inputs['domain'],
'user' => $inputs['user'],
'path' => '/home/'.$inputs['user'].'/'.$inputs['domain'],
]);
}

Expand Down
56 changes: 9 additions & 47 deletions tests/Feature/SitesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,12 @@ public function test_create_site(array $inputs): void
$this->post(route('sites.store', ['server' => $this->server]), $inputs)
->assertSessionDoesntHaveErrors();

$expectedUser = empty($inputs['user']) ? $this->server->getSshUser() : $inputs['user'];
$this->assertDatabaseHas('sites', [
'domain' => $inputs['domain'],
'aliases' => $this->castAsJson($inputs['aliases'] ?? []),
'status' => SiteStatus::READY->value,
'user' => $expectedUser,
'path' => '/home/'.$expectedUser.'/'.$inputs['domain'],
'user' => $inputs['user'],
'path' => '/home/'.$inputs['user'].'/'.$inputs['domain'],
]);
}

Expand Down Expand Up @@ -97,6 +96,7 @@ public function test_create_site_failed_due_to_source_control(int $status): void
'repository' => 'test/test',
'branch' => 'main',
'composer' => true,
'user' => 'example',
];

SSH::fake();
Expand Down Expand Up @@ -421,6 +421,7 @@ public function test_create_site_with_valid_web_directory(): void
'domain' => 'example.com',
'php_version' => '8.2',
'web_directory' => 'public/dist',
'user' => 'example',
])
->assertSessionDoesntHaveErrors();

Expand All @@ -441,6 +442,7 @@ public function test_create_site_with_special_characters_web_directory(): void
'domain' => 'example.com',
'php_version' => '8.2',
'web_directory' => 'public-dist_v1.0',
'user' => 'example',
])
->assertSessionDoesntHaveErrors();

Expand All @@ -461,6 +463,7 @@ public function test_create_site_normalizes_web_directory_slashes(): void
'domain' => 'example.com',
'php_version' => '8.2',
'web_directory' => '/public/',
'user' => 'example',
])
->assertSessionDoesntHaveErrors();

Expand All @@ -481,6 +484,7 @@ public function test_create_site_normalizes_root_web_directory(): void
'domain' => 'example.com',
'php_version' => '8.2',
'web_directory' => '/',
'user' => 'example',
])
->assertSessionDoesntHaveErrors();

Expand All @@ -501,6 +505,7 @@ public function test_create_site_rejects_invalid_web_directory_characters(): voi
'domain' => 'example.com',
'php_version' => '8.2',
'web_directory' => 'public@invalid!',
'user' => 'example',
])
->assertSessionHasErrors(['web_directory']);

Expand All @@ -520,6 +525,7 @@ public function test_create_site_rejects_directory_traversal(): void
'domain' => 'example.com',
'php_version' => '8.2',
'web_directory' => '../etc/passwd',
'user' => 'example',
])
->assertSessionHasErrors(['web_directory']);

Expand Down Expand Up @@ -593,18 +599,6 @@ public static function failure_create_data(): array
public static function create_data(): array
{
return [
[
[
'type' => Laravel::id(),
'domain' => 'example.com',
'aliases' => ['www.example.com', 'www2.example.com'],
'php_version' => '8.2',
'web_directory' => 'public',
'repository' => 'test/test',
'branch' => 'main',
'composer' => true,
],
],
[
[
'type' => Laravel::id(),
Expand All @@ -618,20 +612,6 @@ public static function create_data(): array
'user' => 'example',
],
],
[
[
'type' => Wordpress::id(),
'domain' => 'example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'title' => 'Example',
'username' => 'example',
'email' => '[email protected]',
'password' => 'password',
'database' => '1',
'database_user' => '1',
],
],
[
[
'type' => Wordpress::id(),
Expand All @@ -647,15 +627,6 @@ public static function create_data(): array
'user' => 'example',
],
],
[
[
'type' => PHPBlank::id(),
'domain' => 'example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'web_directory' => 'public',
],
],
[
[
'type' => PHPBlank::id(),
Expand All @@ -666,15 +637,6 @@ public static function create_data(): array
'user' => 'example',
],
],
[
[
'type' => PHPMyAdmin::id(),
'domain' => 'example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'version' => '5.1.2',
],
],
[
[
'type' => PHPMyAdmin::id(),
Expand Down