diff --git a/app/Enums/FirewallRuleStatus.php b/app/Enums/FirewallRuleStatus.php
new file mode 100644
index 0000000..aa316aa
--- /dev/null
+++ b/app/Enums/FirewallRuleStatus.php
@@ -0,0 +1,10 @@
+ $request->location,
'os' => $request->image,
'plan' => $request->server_type,
- 'user' => '',
+ 'user' => 'keystone',
]);
dispatch(new WaitForServerToConnect(
@@ -103,7 +103,7 @@ class ServerController extends Controller
$server = $organisation->servers()->findOrFail($request->route('server'));
return inertia('servers/Show', [
- 'server' => $server->load('services'),
+ 'server' => $server->load('services.slices'),
]);
}
}
diff --git a/app/Jobs/Services/RunStep.php b/app/Jobs/Services/RunStep.php
index 52827ef..13fd47c 100644
--- a/app/Jobs/Services/RunStep.php
+++ b/app/Jobs/Services/RunStep.php
@@ -30,9 +30,7 @@ class RunStep implements ShouldQueue
$server = $this->step->deployment->target->server;
- $ssh = Ssh::create('root', $server->ipv4)
- ->usePrivateKey(storage_path('app/private/ssh/id_ed25519'))
- ->disableStrictHostKeyChecking()
+ $ssh = $server->sshClient()
->onOutput(function ($output) {
$this->step->update([
'logs' => $this->step->logs . "\n" . trim($output),
diff --git a/app/Models/FirewallRule.php b/app/Models/FirewallRule.php
new file mode 100644
index 0000000..2426d19
--- /dev/null
+++ b/app/Models/FirewallRule.php
@@ -0,0 +1,65 @@
+execute();
+ });
+ }
+
+ protected function casts(): array
+ {
+ return [
+ 'status' => FirewallRuleStatus::class,
+ ];
+ }
+
+ public function server(): BelongsTo
+ {
+ return $this->belongsTo(Server::class);
+ }
+
+ public function execute(): void
+ {
+ $ssh = $this->server->sshClient();
+
+ $command = "ufw";
+
+ if ($this->type === 'allow') {
+ $command .= " allow";
+ } elseif ($this->type === 'deny') {
+ $command .= " deny";
+ }
+
+ if ($this->from) {
+ $command .= " from {$this->from}";
+ $command .= " to any port";
+ }
+
+ $command .= " {$this->ports}";
+
+ $result = $ssh->execute($command);
+
+ if (! $result->isSuccessful()) {
+ $this->update([
+ 'status' => FirewallRuleStatus::FAILED,
+ ]);
+ return;
+ }
+ $this->update([
+ 'status' => FirewallRuleStatus::APPLIED,
+ ]);
+ }
+}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 90887e7..e6afdd5 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -7,6 +7,7 @@ use App\Enums\ServerStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Spatie\Ssh\Ssh;
class Server extends Model
{
@@ -29,4 +30,16 @@ class Server extends Model
{
return $this->hasMany(Service::class);
}
+
+ public function firewallRules(): HasMany
+ {
+ return $this->hasMany(FirewallRule::class);
+ }
+
+ public function sshClient(string $user = 'root'): Ssh
+ {
+ return Ssh::create($user, $this->ipv4)
+ ->usePrivateKey(storage_path('app/private/ssh/id_ed25519'))
+ ->disableStrictHostKeyChecking();
+ }
}
diff --git a/database/migrations/2025_03_31_170943_create_firewall_rules_table.php b/database/migrations/2025_03_31_170943_create_firewall_rules_table.php
new file mode 100644
index 0000000..338358d
--- /dev/null
+++ b/database/migrations/2025_03_31_170943_create_firewall_rules_table.php
@@ -0,0 +1,28 @@
+id();
+ $table->string('status')->default(FirewallRuleStatus::NOT_APPLIED->value);
+ $table->foreignIdfor(Server::class);
+ $table->string('type');
+ $table->string('ports');
+ $table->string('from')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('firewall_rules');
+ }
+};
diff --git a/package-lock.json b/package-lock.json
index 524714a..81ae410 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "vue-starter-kit",
+ "name": "keystone",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -16,7 +16,7 @@
"lucide": "^0.468.0",
"lucide-vue-next": "^0.468.0",
"radix-vue": "^1.9.11",
- "tailwind-merge": "^2.5.5",
+ "tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.2.2",
diff --git a/package.json b/package.json
index b6b68f3..9e6c852 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"lucide": "^0.468.0",
"lucide-vue-next": "^0.468.0",
"radix-vue": "^1.9.11",
- "tailwind-merge": "^2.5.5",
+ "tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.2.2",
diff --git a/resources/js/components/ui/badge/Badge.vue b/resources/js/components/ui/badge/Badge.vue
new file mode 100644
index 0000000..9ed8039
--- /dev/null
+++ b/resources/js/components/ui/badge/Badge.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/resources/js/components/ui/badge/index.ts b/resources/js/components/ui/badge/index.ts
new file mode 100644
index 0000000..5834f30
--- /dev/null
+++ b/resources/js/components/ui/badge/index.ts
@@ -0,0 +1,27 @@
+import { cva, type VariantProps } from 'class-variance-authority'
+
+export { default as Badge } from './Badge.vue'
+
+export const badgeVariants = cva(
+ 'inline-flex items-center rounded-full border px-2 py-0.25 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ destructive:
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ success:
+ 'border-transparent bg-green-200 text-green-800 hover:bg-green-200/80',
+ outline: 'text-foreground',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+)
+
+export type BadgeVariants = VariantProps
diff --git a/resources/js/pages/servers/Show.vue b/resources/js/pages/servers/Show.vue
index 61f5be1..e5670e5 100644
--- a/resources/js/pages/servers/Show.vue
+++ b/resources/js/pages/servers/Show.vue
@@ -1,6 +1,9 @@