wip: boarding
This commit is contained in:
parent
2414ddd360
commit
b39ca51d41
1
.ssh/known_hosts
Normal file
1
.ssh/known_hosts
Normal file
@ -0,0 +1 @@
|
||||
|1|TRkudHmvsBVekjyAfpo3EWrkQSs=|XdMLpIt2l32hhdSyWnDwMMlVBSI= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK/Ln1b72Lc5JtHRDiZd4lYyW7F5aVuJH42HdHXuYudT
|
@ -17,9 +17,6 @@ public function __invoke(bool $force)
|
||||
$settings = InstanceSettings::get();
|
||||
ray('Running InstanceAutoUpdateJob');
|
||||
$localhost_name = 'localhost';
|
||||
if (is_dev()) {
|
||||
$localhost_name = 'testing-local-docker-container';
|
||||
}
|
||||
$this->server = Server::where('name', $localhost_name)->firstOrFail();
|
||||
$this->latest_version = get_latest_version_of_coolify();
|
||||
$this->current_version = config('version');
|
||||
|
@ -12,7 +12,7 @@ class ApplicationController extends Controller
|
||||
|
||||
public function configuration()
|
||||
{
|
||||
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@ -29,7 +29,7 @@ public function configuration()
|
||||
|
||||
public function deployments()
|
||||
{
|
||||
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@ -49,7 +49,7 @@ public function deployment()
|
||||
{
|
||||
$deploymentUuid = request()->route('deployment_uuid');
|
||||
|
||||
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
@ -58,7 +58,6 @@ public function dashboard()
|
||||
$resources += $project->applications->count();
|
||||
$resources += $project->postgresqls->count();
|
||||
}
|
||||
|
||||
return view('dashboard', [
|
||||
'servers' => $servers->count(),
|
||||
'projects' => $projects->count(),
|
||||
@ -66,10 +65,17 @@ public function dashboard()
|
||||
's3s' => $s3s,
|
||||
]);
|
||||
}
|
||||
public function boarding() {
|
||||
if (currentTeam()->boarding || is_dev()) {
|
||||
return view('boarding');
|
||||
} else {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
public function settings()
|
||||
{
|
||||
if (is_instance_admin()) {
|
||||
if (isInstanceAdmin()) {
|
||||
$settings = InstanceSettings::get();
|
||||
$database = StandalonePostgresql::whereName('coolify-db')->first();
|
||||
if ($database) {
|
||||
@ -89,7 +95,7 @@ public function team()
|
||||
{
|
||||
$invitations = [];
|
||||
if (auth()->user()->isAdminFromSession()) {
|
||||
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
|
||||
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
|
||||
}
|
||||
return view('team.show', [
|
||||
'invitations' => $invitations,
|
||||
@ -116,7 +122,7 @@ public function members()
|
||||
{
|
||||
$invitations = [];
|
||||
if (auth()->user()->isAdminFromSession()) {
|
||||
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
|
||||
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
|
||||
}
|
||||
return view('team.members', [
|
||||
'invitations' => $invitations,
|
||||
|
@ -11,7 +11,7 @@ class DatabaseController extends Controller
|
||||
|
||||
public function configuration()
|
||||
{
|
||||
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@ -29,7 +29,7 @@ public function configuration()
|
||||
public function executions()
|
||||
{
|
||||
$backup_uuid = request()->route('backup_uuid');
|
||||
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@ -50,13 +50,13 @@ public function executions()
|
||||
'database' => $database,
|
||||
'backup' => $backup,
|
||||
'executions' => $executions,
|
||||
's3s' => auth()->user()->currentTeam()->s3s,
|
||||
's3s' => currentTeam()->s3s,
|
||||
]);
|
||||
}
|
||||
|
||||
public function backups()
|
||||
{
|
||||
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@ -70,7 +70,7 @@ public function backups()
|
||||
}
|
||||
return view('project.database.backups.all', [
|
||||
'database' => $database,
|
||||
's3s' => auth()->user()->currentTeam()->s3s,
|
||||
's3s' => currentTeam()->s3s,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ public function newProject()
|
||||
{
|
||||
$project = Project::firstOrCreate(
|
||||
['name' => request()->query('name') ?? generate_random_name()],
|
||||
['team_id' => auth()->user()->currentTeam()->id]
|
||||
['team_id' => currentTeam()->id]
|
||||
);
|
||||
return response()->json([
|
||||
'project_uuid' => $project->uuid
|
||||
@ -68,7 +68,7 @@ public function newTeam()
|
||||
],
|
||||
);
|
||||
auth()->user()->teams()->attach($team, ['role' => 'admin']);
|
||||
session(['currentTeam' => $team]);
|
||||
refreshSession();
|
||||
return redirect(request()->header('Referer'));
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ public function all()
|
||||
public function edit()
|
||||
{
|
||||
$projectUuid = request()->route('project_uuid');
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
$teamId = currentTeam()->id;
|
||||
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
@ -29,7 +29,7 @@ public function edit()
|
||||
public function show()
|
||||
{
|
||||
$projectUuid = request()->route('project_uuid');
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
$teamId = currentTeam()->id;
|
||||
|
||||
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
|
||||
if (!$project) {
|
||||
@ -44,7 +44,7 @@ public function new()
|
||||
$type = request()->query('type');
|
||||
$destination_uuid = request()->query('destination');
|
||||
|
||||
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@ -67,7 +67,7 @@ public function new()
|
||||
|
||||
public function resources()
|
||||
{
|
||||
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
@ -12,14 +12,14 @@ class ServerController extends Controller
|
||||
|
||||
public function new_server()
|
||||
{
|
||||
if (!is_cloud() || is_instance_admin()) {
|
||||
if (!is_cloud() || isInstanceAdmin()) {
|
||||
return view('server.create', [
|
||||
'limit_reached' => false,
|
||||
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
|
||||
]);
|
||||
}
|
||||
$servers = auth()->user()->currentTeam()->servers->count();
|
||||
$subscription = auth()->user()->currentTeam()?->subscription->type();
|
||||
$servers = currentTeam()->servers->count();
|
||||
$subscription = currentTeam()?->subscription->type();
|
||||
$your_limit = config('constants.limits.server')[strtolower($subscription)];
|
||||
$limit_reached = $servers >= $your_limit;
|
||||
|
||||
|
@ -39,6 +39,7 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\CheckForcePasswordReset::class,
|
||||
\App\Http\Middleware\SubscriptionValid::class,
|
||||
\App\Http\Middleware\IsBoardingFlow::class,
|
||||
|
||||
],
|
||||
|
||||
|
147
app/Http/Livewire/Boarding.php
Normal file
147
app/Http/Livewire/Boarding.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Boarding extends Component
|
||||
{
|
||||
|
||||
public string $currentState = 'create-private-key';
|
||||
// public ?string $serverType = null;
|
||||
|
||||
public ?string $privateKeyType = null;
|
||||
public ?string $privateKey = null;
|
||||
public ?string $privateKeyName = null;
|
||||
public ?string $privateKeyDescription = null;
|
||||
public ?PrivateKey $createdPrivateKey = null;
|
||||
|
||||
public ?string $remoteServerName = null;
|
||||
public ?string $remoteServerDescription = null;
|
||||
public ?string $remoteServerHost = null;
|
||||
public ?int $remoteServerPort = 22;
|
||||
public ?string $remoteServerUser = 'root';
|
||||
public ?Server $createdServer = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->privateKeyName = generate_random_name();
|
||||
$this->remoteServerName = generate_random_name();
|
||||
if (is_dev()) {
|
||||
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
|
||||
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
|
||||
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
|
||||
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
-----END OPENSSH PRIVATE KEY-----';
|
||||
$this->privateKeyDescription = 'Created by Coolify';
|
||||
$this->remoteServerDescription = 'Created by Coolify';
|
||||
$this->remoteServerHost = 'coolify-testing-host';
|
||||
}
|
||||
}
|
||||
public function restartBoarding()
|
||||
{
|
||||
if ($this->createdServer) {
|
||||
$this->createdServer->delete();
|
||||
}
|
||||
if ($this->createdPrivateKey) {
|
||||
$this->createdPrivateKey->delete();
|
||||
}
|
||||
return redirect()->route('boarding');
|
||||
}
|
||||
public function skipBoarding()
|
||||
{
|
||||
currentTeam()->update([
|
||||
'show_boarding' => false
|
||||
]);
|
||||
refreshSession();
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
public function setServer(string $type)
|
||||
{
|
||||
if ($type === 'localhost') {
|
||||
$this->currentState = 'create-project';
|
||||
} elseif ($type === 'remote') {
|
||||
$this->currentState = 'private-key';
|
||||
}
|
||||
}
|
||||
public function setPrivateKey(string $type)
|
||||
{
|
||||
$this->privateKeyType = $type;
|
||||
$this->currentState = 'create-private-key';
|
||||
}
|
||||
public function savePrivateKey()
|
||||
{
|
||||
$this->validate([
|
||||
'privateKeyName' => 'required',
|
||||
'privateKey' => 'required',
|
||||
]);
|
||||
$this->currentState = 'create-server';
|
||||
}
|
||||
public function saveServer()
|
||||
{
|
||||
$this->validate([
|
||||
'remoteServerName' => 'required',
|
||||
'remoteServerHost' => 'required',
|
||||
'remoteServerPort' => 'required',
|
||||
'remoteServerUser' => 'required',
|
||||
]);
|
||||
if ($this->privateKeyType === 'create') {
|
||||
$this->createNewPrivateKey();
|
||||
}
|
||||
$this->privateKey = formatPrivateKey($this->privateKey);
|
||||
$this->createdPrivateKey = PrivateKey::create([
|
||||
'name' => $this->privateKeyName,
|
||||
'description' => $this->privateKeyDescription,
|
||||
'private_key' => $this->privateKey,
|
||||
'team_id' => currentTeam()->id
|
||||
]);
|
||||
$this->createdServer = Server::create([
|
||||
'name' => $this->remoteServerName,
|
||||
'ip' => $this->remoteServerHost,
|
||||
'port' => $this->remoteServerPort,
|
||||
'user' => $this->remoteServerUser,
|
||||
'description' => $this->remoteServerDescription,
|
||||
'private_key_id' => $this->createdPrivateKey->id,
|
||||
'team_id' => currentTeam()->id
|
||||
]);
|
||||
try {
|
||||
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer);
|
||||
if (!$uptime) {
|
||||
$this->createdServer->delete();
|
||||
$this->createdPrivateKey->delete();
|
||||
throw new \Exception('Server is not reachable.');
|
||||
} else {
|
||||
$this->createdServer->settings->update([
|
||||
'is_reachable' => true,
|
||||
]);
|
||||
$this->emit('success', 'Server is reachable.');
|
||||
}
|
||||
if ($dockerVersion) {
|
||||
$this->emit('error', 'Docker is not installed on the server.');
|
||||
$this->currentState = 'install-docker';
|
||||
return;
|
||||
}
|
||||
ray($uptime, $dockerVersion);
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
|
||||
}
|
||||
}
|
||||
private function createNewPrivateKey()
|
||||
{
|
||||
$this->privateKeyName = generate_random_name();
|
||||
$this->privateKeyDescription = 'Created by Coolify';
|
||||
$this->privateKey = generateSSHKey();
|
||||
}
|
||||
public function createNewProject()
|
||||
{
|
||||
Project::create([
|
||||
'name' => generate_random_name(),
|
||||
'team_id' => currentTeam()->id
|
||||
]);
|
||||
}
|
||||
}
|
@ -67,7 +67,7 @@ public function submit()
|
||||
'name' => $this->name,
|
||||
'network' => $this->network,
|
||||
'server_id' => $this->server_id,
|
||||
'team_id' => auth()->user()->currentTeam()->id
|
||||
'team_id' => currentTeam()->id
|
||||
]);
|
||||
}
|
||||
$this->createNetworkAndAttachToProxy();
|
||||
|
@ -41,10 +41,9 @@ public function submit()
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
ray($this->model);
|
||||
$this->model->save();
|
||||
if (is_a($this->model, Team::class)) {
|
||||
session(['currentTeam' => $this->model]);
|
||||
refreshSession();
|
||||
}
|
||||
$this->emit('success', 'Settings saved.');
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ public function copyFromInstanceSettings()
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
if ($settings->smtp_enabled) {
|
||||
$team = auth()->user()->currentTeam();
|
||||
$team = currentTeam();
|
||||
$team->update([
|
||||
'smtp_enabled' => $settings->smtp_enabled,
|
||||
'smtp_from_address' => $settings->smtp_from_address,
|
||||
@ -74,7 +74,7 @@ public function copyFromInstanceSettings()
|
||||
]);
|
||||
$this->decrypt();
|
||||
if (is_a($team, Team::class)) {
|
||||
session(['currentTeam' => $team]);
|
||||
refreshSession();
|
||||
}
|
||||
$this->model = $team;
|
||||
$this->emit('success', 'Settings saved.');
|
||||
@ -119,7 +119,7 @@ public function saveModel()
|
||||
$this->model->save();
|
||||
$this->decrypt();
|
||||
if (is_a($this->model, Team::class)) {
|
||||
session(['currentTeam' => $this->model]);
|
||||
refreshSession();
|
||||
}
|
||||
$this->emit('success', 'Settings saved.');
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ public function delete()
|
||||
try {
|
||||
if ($this->private_key->isEmpty()) {
|
||||
$this->private_key->delete();
|
||||
auth()->user()->currentTeam()->privateKeys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->get();
|
||||
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
|
||||
return redirect()->route('private-key.all');
|
||||
}
|
||||
$this->emit('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
|
||||
@ -38,10 +38,7 @@ public function delete()
|
||||
public function changePrivateKey()
|
||||
{
|
||||
try {
|
||||
$this->private_key->private_key = trim($this->private_key->private_key);
|
||||
if (!str_ends_with($this->private_key->private_key, "\n")) {
|
||||
$this->private_key->private_key .= "\n";
|
||||
}
|
||||
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
|
||||
$this->private_key->save();
|
||||
refresh_server_connection($this->private_key);
|
||||
} catch (\Exception $e) {
|
||||
|
@ -32,7 +32,7 @@ public function createPrivateKey()
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'private_key' => $this->value,
|
||||
'team_id' => auth()->user()->currentTeam()->id
|
||||
'team_id' => currentTeam()->id
|
||||
]);
|
||||
if ($this->from === 'server') {
|
||||
return redirect()->route('server.create');
|
||||
|
@ -25,7 +25,7 @@ public function submit()
|
||||
$project = Project::create([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'team_id' => auth()->user()->currentTeam()->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
return redirect()->route('project.show', $project->uuid);
|
||||
} catch (\Exception $e) {
|
||||
|
@ -29,7 +29,7 @@ public function mount()
|
||||
|
||||
private function get_private_keys()
|
||||
{
|
||||
$this->private_keys = PrivateKey::whereTeamId(auth()->user()->currentTeam()->id)->get()->reject(function ($key) {
|
||||
$this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) {
|
||||
return $key->id == $this->application->private_key_id;
|
||||
});
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ public function submit(): void
|
||||
's3_storage_id' => $this->s3_storage_id,
|
||||
'database_id' => $this->database->id,
|
||||
'database_type' => $this->database->getMorphClass(),
|
||||
'team_id' => auth()->user()->currentTeam()->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$this->emit('refreshScheduledBackups');
|
||||
} catch (\Exception $e) {
|
||||
|
@ -11,7 +11,7 @@ public function createEmptyProject()
|
||||
{
|
||||
$project = Project::create([
|
||||
'name' => generate_random_name(),
|
||||
'team_id' => auth()->user()->currentTeam()->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']);
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ public function mount()
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->query = request()->query();
|
||||
$this->private_keys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->where('id', '!=', 0)->get();
|
||||
$this->private_keys = PrivateKey::where('team_id', currentTeam()->id)->where('id', '!=', 0)->get();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
|
@ -42,7 +42,7 @@ public function mount()
|
||||
|
||||
public function installDocker()
|
||||
{
|
||||
$activity = resolve(InstallDocker::class)($this->server, auth()->user()->currentTeam());
|
||||
$activity = resolve(InstallDocker::class)($this->server, currentTeam());
|
||||
$this->emit('newMonitorActivity', $activity->id);
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ public function submit()
|
||||
'ip' => $this->ip,
|
||||
'user' => $this->user,
|
||||
'port' => $this->port,
|
||||
'team_id' => auth()->user()->currentTeam()->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
]);
|
||||
$server->settings->is_part_of_swarm = $this->is_part_of_swarm;
|
||||
|
@ -65,7 +65,7 @@ public function add_coolify_database()
|
||||
'frequency' => '0 0 * * *',
|
||||
'database_id' => $this->database->id,
|
||||
'database_type' => 'App\Models\StandalonePostgresql',
|
||||
'team_id' => auth()->user()->currentTeam()->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$this->database->refresh();
|
||||
$this->backup->refresh();
|
||||
|
@ -40,7 +40,7 @@ public function createGitHubApp()
|
||||
'custom_user' => $this->custom_user,
|
||||
'custom_port' => $this->custom_port,
|
||||
'is_system_wide' => $this->is_system_wide,
|
||||
'team_id' => auth()->user()->currentTeam()->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
|
||||
} catch (\Exception $e) {
|
||||
|
@ -10,7 +10,7 @@ class Actions extends Component
|
||||
public function cancel()
|
||||
{
|
||||
try {
|
||||
$subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
|
||||
$subscription_id = currentTeam()->subscription->lemon_subscription_id;
|
||||
if (!$subscription_id) {
|
||||
throw new \Exception('No subscription found');
|
||||
}
|
||||
@ -37,7 +37,7 @@ public function cancel()
|
||||
public function resume()
|
||||
{
|
||||
try {
|
||||
$subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
|
||||
$subscription_id = currentTeam()->subscription->lemon_subscription_id;
|
||||
if (!$subscription_id) {
|
||||
throw new \Exception('No subscription found');
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public function submit()
|
||||
'personal_team' => false,
|
||||
]);
|
||||
auth()->user()->teams()->attach($team, ['role' => 'admin']);
|
||||
session(['currentTeam' => $team]);
|
||||
refreshSession();
|
||||
return redirect()->route('team.show');
|
||||
} catch (\Throwable $th) {
|
||||
return general_error_handler($th, $this);
|
||||
|
@ -9,7 +9,7 @@ class Delete extends Component
|
||||
{
|
||||
public function delete()
|
||||
{
|
||||
$currentTeam = auth()->user()->currentTeam();
|
||||
$currentTeam = currentTeam();
|
||||
$currentTeam->delete();
|
||||
|
||||
$team = auth()->user()->teams()->first();
|
||||
@ -24,7 +24,7 @@ public function delete()
|
||||
}
|
||||
});
|
||||
|
||||
session(['currentTeam' => $team]);
|
||||
refreshSession();
|
||||
return redirect()->route('team.show');
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ class Form extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->team = auth()->user()->currentTeam();
|
||||
$this->team = currentTeam();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
@ -27,7 +27,7 @@ public function submit()
|
||||
$this->validate();
|
||||
try {
|
||||
$this->team->save();
|
||||
session(['currentTeam' => $this->team]);
|
||||
refreshSession();
|
||||
$this->emit('reloadWindow');
|
||||
} catch (\Throwable $th) {
|
||||
return general_error_handler($th, $this);
|
||||
|
@ -18,6 +18,6 @@ public function deleteInvitation(int $invitation_id)
|
||||
|
||||
public function refreshInvitations()
|
||||
{
|
||||
$this->invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
|
||||
$this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
|
||||
}
|
||||
}
|
||||
|
@ -35,9 +35,9 @@ private function generate_invite_link(bool $isEmail = false)
|
||||
return general_error_handler(that: $this, customErrorMessage: "$this->email must be registered first (or activate transactional emails to invite via email).");
|
||||
}
|
||||
|
||||
$member_emails = auth()->user()->currentTeam()->members()->get()->pluck('email');
|
||||
$member_emails = currentTeam()->members()->get()->pluck('email');
|
||||
if ($member_emails->contains($this->email)) {
|
||||
return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . auth()->user()->currentTeam()->name . ".");
|
||||
return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . ".");
|
||||
}
|
||||
|
||||
$invitation = TeamInvitation::whereEmail($this->email);
|
||||
@ -53,7 +53,7 @@ private function generate_invite_link(bool $isEmail = false)
|
||||
}
|
||||
|
||||
TeamInvitation::firstOrCreate([
|
||||
'team_id' => auth()->user()->currentTeam()->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
'uuid' => $uuid,
|
||||
'email' => $this->email,
|
||||
'role' => $this->role,
|
||||
|
@ -11,19 +11,19 @@ class Member extends Component
|
||||
|
||||
public function makeAdmin()
|
||||
{
|
||||
$this->member->teams()->updateExistingPivot(auth()->user()->currentTeam()->id, ['role' => 'admin']);
|
||||
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']);
|
||||
$this->emit('reloadWindow');
|
||||
}
|
||||
|
||||
public function makeReadonly()
|
||||
{
|
||||
$this->member->teams()->updateExistingPivot(auth()->user()->currentTeam()->id, ['role' => 'member']);
|
||||
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
|
||||
$this->emit('reloadWindow');
|
||||
}
|
||||
|
||||
public function remove()
|
||||
{
|
||||
$this->member->teams()->detach(auth()->user()->currentTeam());
|
||||
$this->member->teams()->detach(currentTeam());
|
||||
$this->emit('reloadWindow');
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ public function submit()
|
||||
} else {
|
||||
$this->storage->endpoint = $this->endpoint;
|
||||
}
|
||||
$this->storage->team_id = auth()->user()->currentTeam()->id;
|
||||
$this->storage->team_id = currentTeam()->id;
|
||||
$this->storage->testConnection();
|
||||
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
|
||||
$this->storage->save();
|
||||
|
28
app/Http/Middleware/IsBoardingFlow.php
Normal file
28
app/Http/Middleware/IsBoardingFlow.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IsBoardingFlow
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$allowed_paths = [
|
||||
'subscription',
|
||||
'boarding',
|
||||
'livewire/message/boarding'
|
||||
];
|
||||
if (showBoarding() && !in_array($request->path(), $allowed_paths)) {
|
||||
return redirect('boarding');
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -17,8 +17,7 @@ public function handle(Request $request, Closure $next): Response
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
$is_instance_admin = is_instance_admin();
|
||||
if ($is_instance_admin) {
|
||||
if (isInstanceAdmin()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ protected function value(): Attribute
|
||||
|
||||
private function get_environment_variables(string $environment_variable): string|null
|
||||
{
|
||||
// $team_id = auth()->user()->currentTeam()->id;
|
||||
// $team_id = currentTeam()->id;
|
||||
if (str_contains(trim($environment_variable), '{{') && str_contains(trim($environment_variable), '}}')) {
|
||||
$environment_variable = preg_replace('/\s+/', '', $environment_variable);
|
||||
$environment_variable = str_replace('{{', '', $environment_variable);
|
||||
|
@ -19,12 +19,12 @@ class GithubApp extends BaseModel
|
||||
|
||||
static public function public()
|
||||
{
|
||||
return GithubApp::whereTeamId(auth()->user()->currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
|
||||
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
|
||||
}
|
||||
|
||||
static public function private()
|
||||
{
|
||||
return GithubApp::whereTeamId(auth()->user()->currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
|
||||
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
|
@ -16,7 +16,7 @@ class PrivateKey extends BaseModel
|
||||
static public function ownedByCurrentTeam(array $select = ['*'])
|
||||
{
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
return PrivateKey::whereTeamId(auth()->user()->currentTeam()->id)->select($selectArray->all());
|
||||
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
|
@ -4,16 +4,11 @@
|
||||
|
||||
class Project extends BaseModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'team_id',
|
||||
'project_id'
|
||||
];
|
||||
protected $guarded = [];
|
||||
|
||||
static public function ownedByCurrentTeam()
|
||||
{
|
||||
return Project::whereTeamId(auth()->user()->currentTeam()->id)->orderBy('name');
|
||||
return Project::whereTeamId(currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
|
@ -17,7 +17,7 @@ class S3Storage extends BaseModel
|
||||
static public function ownedByCurrentTeam(array $select = ['*'])
|
||||
{
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
return S3Storage::whereTeamId(auth()->user()->currentTeam()->id)->select($selectArray->all())->orderBy('name');
|
||||
return S3Storage::whereTeamId(currentTeam()->id)->select($selectArray->all())->orderBy('name');
|
||||
}
|
||||
|
||||
public function awsUrl()
|
||||
|
@ -33,7 +33,7 @@ static public function isReachable()
|
||||
|
||||
static public function ownedByCurrentTeam(array $select = ['*'])
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
$teamId = currentTeam()->id;
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
return Server::whereTeamId($teamId)->with('settings')->select($selectArray->all())->orderBy('name');
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'force_password_reset' => 'boolean',
|
||||
'show_boarding' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
@ -31,6 +32,7 @@ protected static function boot()
|
||||
$team = [
|
||||
'name' => $user->name . "'s Team",
|
||||
'personal_team' => true,
|
||||
'show_boarding' => true,
|
||||
];
|
||||
if ($user->id === 0) {
|
||||
$team['id'] = 0;
|
||||
@ -102,7 +104,7 @@ public function currentTeam()
|
||||
|
||||
public function otherTeams()
|
||||
{
|
||||
$team_id = auth()->user()->currentTeam()->id;
|
||||
$team_id = currentTeam()->id;
|
||||
return auth()->user()->teams->filter(function ($team) use ($team_id) {
|
||||
return $team->id != $team_id;
|
||||
});
|
||||
@ -113,13 +115,6 @@ public function role()
|
||||
if ($this->teams()->where('team_id', 0)->first()) {
|
||||
return 'admin';
|
||||
}
|
||||
return $this->teams()->where('team_id', auth()->user()->currentTeam()->id)->first()->pivot->role;
|
||||
}
|
||||
|
||||
public function resources()
|
||||
{
|
||||
$team_id = auth()->user()->currentTeam()->id;
|
||||
$data = Application::where('team_id', $team_id)->get();
|
||||
return $data;
|
||||
return $this->teams()->where('team_id', currentTeam()->id)->first()->pivot->role;
|
||||
}
|
||||
}
|
||||
|
@ -66,8 +66,6 @@ function get_private_key_for_server(Server $server)
|
||||
function save_private_key_for_server(Server $server)
|
||||
{
|
||||
if (data_get($server, 'privateKey.private_key') === null) {
|
||||
$server->settings->is_reachable = false;
|
||||
$server->settings->save();
|
||||
throw new \Exception("Server {$server->name} does not have a private key");
|
||||
}
|
||||
$temp_file = "id.root@{$server->ip}";
|
||||
@ -159,8 +157,8 @@ function refresh_server_connection(PrivateKey $private_key)
|
||||
// Delete the old ssh mux file to force a new one to be created
|
||||
Storage::disk('ssh-mux')->delete($server->muxFilename());
|
||||
// check if user is authenticated
|
||||
if (auth()?->user()?->currentTeam()->id) {
|
||||
auth()->user()->currentTeam()->privateKeys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->get();
|
||||
if (currentTeam()->id) {
|
||||
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
use Nubs\RandomNameGenerator\All;
|
||||
use Poliander\Cron\CronExpression;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
use phpseclib3\Crypt\RSA;
|
||||
|
||||
function application_configuration_dir(): string
|
||||
{
|
||||
@ -35,11 +36,25 @@ function generate_readme_file(string $name, string $updated_at): string
|
||||
return "Resource name: $name\nLatest Deployment Date: $updated_at";
|
||||
}
|
||||
|
||||
function is_instance_admin()
|
||||
function isInstanceAdmin()
|
||||
{
|
||||
return auth()->user()?->isInstanceAdmin();
|
||||
return auth()?->user()?->isInstanceAdmin() ?? false;
|
||||
}
|
||||
|
||||
function currentTeam()
|
||||
{
|
||||
return auth()?->user()?->currentTeam() ?? null;
|
||||
}
|
||||
|
||||
function showBoarding(): bool
|
||||
{
|
||||
return currentTeam()->show_boarding ?? false;
|
||||
}
|
||||
function refreshSession(): void
|
||||
{
|
||||
$team = currentTeam();
|
||||
session(['currentTeam' => $team]);
|
||||
}
|
||||
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
|
||||
{
|
||||
try {
|
||||
@ -97,7 +112,21 @@ function generate_random_name(): string
|
||||
$cuid = new Cuid2(7);
|
||||
return Str::kebab("{$generator->getName()}-$cuid");
|
||||
}
|
||||
|
||||
function generateSSHKey()
|
||||
{
|
||||
$key = RSA::createKey();
|
||||
return [
|
||||
'private' => $key->toString('PKCS1'),
|
||||
'public' => $key->getPublicKey()->toString('OpenSSH',['comment' => 'coolify-generated-ssh-key'])
|
||||
];
|
||||
}
|
||||
function formatPrivateKey(string $privateKey) {
|
||||
$privateKey = trim($privateKey);
|
||||
if (!str_ends_with($privateKey, "\n")) {
|
||||
$privateKey .= "\n";
|
||||
}
|
||||
return $privateKey;
|
||||
}
|
||||
function generate_application_name(string $git_repository, string $git_branch): string
|
||||
{
|
||||
$cuid = new Cuid2(7);
|
||||
|
@ -9,7 +9,7 @@ function getSubscriptionLink($type)
|
||||
return null;
|
||||
}
|
||||
$user_id = auth()->user()->id;
|
||||
$team_id = auth()->user()->currentTeam()->id ?? null;
|
||||
$team_id = currentTeam()->id ?? null;
|
||||
$email = auth()->user()->email ?? null;
|
||||
$name = auth()->user()->name ?? null;
|
||||
$url = "https://store.coollabs.io/checkout/buy/$checkout_id?";
|
||||
@ -30,27 +30,27 @@ function getSubscriptionLink($type)
|
||||
|
||||
function getPaymentLink()
|
||||
{
|
||||
return auth()->user()->currentTeam()->subscription->lemon_update_payment_menthod_url;
|
||||
return currentTeam()->subscription->lemon_update_payment_menthod_url;
|
||||
}
|
||||
|
||||
function getRenewDate()
|
||||
{
|
||||
return Carbon::parse(auth()->user()->currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
|
||||
return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
|
||||
}
|
||||
|
||||
function getEndDate()
|
||||
{
|
||||
return Carbon::parse(auth()->user()->currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
|
||||
return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
|
||||
}
|
||||
|
||||
function is_subscription_active()
|
||||
{
|
||||
$team = auth()->user()?->currentTeam();
|
||||
$team = currentTeam();
|
||||
|
||||
if (!$team) {
|
||||
return false;
|
||||
}
|
||||
if (is_instance_admin()) {
|
||||
if (isInstanceAdmin()) {
|
||||
return true;
|
||||
}
|
||||
$subscription = $team?->subscription;
|
||||
@ -64,11 +64,11 @@ function is_subscription_active()
|
||||
}
|
||||
function is_subscription_in_grace_period()
|
||||
{
|
||||
$team = auth()->user()?->currentTeam();
|
||||
$team = currentTeam();
|
||||
if (!$team) {
|
||||
return false;
|
||||
}
|
||||
if (is_instance_admin()) {
|
||||
if (isInstanceAdmin()) {
|
||||
return true;
|
||||
}
|
||||
$subscription = $team?->subscription;
|
||||
|
@ -23,6 +23,7 @@
|
||||
"livewire/livewire": "^v2.12.3",
|
||||
"masmerise/livewire-toaster": "^1.2",
|
||||
"nubs/random-name-generator": "^2.2",
|
||||
"phpseclib/phpseclib": "~3.0",
|
||||
"poliander/cron": "^3.0",
|
||||
"resend/resend-laravel": "^0.5.0",
|
||||
"sentry/sentry-laravel": "^3.4",
|
||||
|
162
composer.lock
generated
162
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "15eb89e93c667bb63f48ef548ed38bed",
|
||||
"content-hash": "a4143cdb58c02a0490f9aa03d05b8e9a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@ -3875,6 +3875,56 @@
|
||||
},
|
||||
"time": "2022-06-14T06:56:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/random_compat",
|
||||
"version": "v9.99.100",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/random_compat.git",
|
||||
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">= 7"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.*|5.*",
|
||||
"vimeo/psalm": "^1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
|
||||
},
|
||||
"type": "library",
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paragon Initiative Enterprises",
|
||||
"email": "security@paragonie.com",
|
||||
"homepage": "https://paragonie.com"
|
||||
}
|
||||
],
|
||||
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
|
||||
"keywords": [
|
||||
"csprng",
|
||||
"polyfill",
|
||||
"pseudorandom",
|
||||
"random"
|
||||
],
|
||||
"support": {
|
||||
"email": "info@paragonie.com",
|
||||
"issues": "https://github.com/paragonie/random_compat/issues",
|
||||
"source": "https://github.com/paragonie/random_compat"
|
||||
},
|
||||
"time": "2020-10-15T08:29:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/client-common",
|
||||
"version": "2.7.0",
|
||||
@ -4446,6 +4496,116 @@
|
||||
],
|
||||
"time": "2023-02-25T19:38:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpseclib/phpseclib",
|
||||
"version": "3.0.21",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||
"reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/4580645d3fc05c189024eb3b834c6c1e4f0f30a1",
|
||||
"reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"paragonie/constant_time_encoding": "^1|^2",
|
||||
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
|
||||
"php": ">=5.6.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-dom": "Install the DOM extension to load XML formatted public keys.",
|
||||
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
|
||||
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
|
||||
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
|
||||
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"phpseclib/bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"phpseclib3\\": "phpseclib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jim Wigginton",
|
||||
"email": "terrafrost@php.net",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Patrick Monnerat",
|
||||
"email": "pm@datasphere.ch",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Andreas Fischer",
|
||||
"email": "bantu@phpbb.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Hans-Jürgen Petrich",
|
||||
"email": "petrich@tronic-media.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "graham@alt-three.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
|
||||
"homepage": "http://phpseclib.sourceforge.net",
|
||||
"keywords": [
|
||||
"BigInteger",
|
||||
"aes",
|
||||
"asn.1",
|
||||
"asn1",
|
||||
"blowfish",
|
||||
"crypto",
|
||||
"cryptography",
|
||||
"encryption",
|
||||
"rsa",
|
||||
"security",
|
||||
"sftp",
|
||||
"signature",
|
||||
"signing",
|
||||
"ssh",
|
||||
"twofish",
|
||||
"x.509",
|
||||
"x509"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.21"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/terrafrost",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpseclib",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-09T15:24:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "1.23.1",
|
||||
|
@ -0,0 +1,28 @@
|
||||
<?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::table('teams', function (Blueprint $table) {
|
||||
$table->boolean('show_boarding')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->dropColumn('show_boarding');
|
||||
});
|
||||
}
|
||||
};
|
@ -11,8 +11,8 @@ public function run(): void
|
||||
{
|
||||
Server::create([
|
||||
'id' => 0,
|
||||
'name' => "testing-local-docker-container",
|
||||
'description' => "This is a test docker container",
|
||||
'name' => "localhost",
|
||||
'description' => "This is a test docker container in development mode",
|
||||
'ip' => "coolify-testing-host",
|
||||
'team_id' => 0,
|
||||
'private_key_id' => 0,
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
|
@ -53,7 +53,7 @@ .icon:hover {
|
||||
@apply text-white;
|
||||
}
|
||||
.box {
|
||||
@apply flex items-center justify-center p-2 transition-colors rounded min-h-12 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline;
|
||||
@apply flex items-center justify-center p-2 transition-colors rounded cursor-pointer min-h-12 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline;
|
||||
}
|
||||
|
||||
.lds-heart {
|
||||
|
@ -53,12 +53,12 @@
|
||||
<span v-if="search"><span class="capitalize ">{{
|
||||
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
|
||||
will be:
|
||||
<span class="text-warning">{{ search
|
||||
}}</span></span>
|
||||
<x-highlighted text="{{ search }}" />
|
||||
</span>
|
||||
<span v-else><span class="capitalize ">{{
|
||||
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
|
||||
will be:
|
||||
<span class="text-warning">randomly generated (type to change)</span>
|
||||
<x-highlighted text="randomly generated (type to change)" />
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
|
3
resources/views/boarding.blade.php
Normal file
3
resources/views/boarding.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<x-layout-simple>
|
||||
<livewire:boarding />
|
||||
</x-layout-simple>
|
25
resources/views/components/boarding-step.blade.php
Normal file
25
resources/views/components/boarding-step.blade.php
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="box-border col-span-2 min-w-[24rem]">
|
||||
<h1 class="text-5xl font-bold">{{$title}}</h1>
|
||||
<div class="py-6 ">
|
||||
@isset($question)
|
||||
<p class="text-base">
|
||||
{{$question}}
|
||||
</p>
|
||||
@endisset
|
||||
</div>
|
||||
@if($actions)
|
||||
<div class="flex flex-col gap-4 md:flex-row">
|
||||
{{$actions}}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if($explanation)
|
||||
<div class="col-span-1">
|
||||
<h1 class="pb-8 font-bold">Explanation</h1>
|
||||
<div class="space-y-4">
|
||||
{{$explanation}}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
@ -2,7 +2,7 @@
|
||||
@if ($label)
|
||||
<label for="small-input" class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
|
||||
@if ($required)
|
||||
<span class="text-warning">*</span>
|
||||
<x-highlighted text="*" />
|
||||
@endif
|
||||
@if ($helper)
|
||||
<x-helper :helper="$helper" />
|
||||
|
@ -2,7 +2,7 @@
|
||||
@if ($label)
|
||||
<label for="small-input" class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
|
||||
@if ($required)
|
||||
<span class="text-warning">*</span>
|
||||
<x-highlighted text="*" />
|
||||
@endif
|
||||
@if ($helper)
|
||||
<x-helper :helper="$helper" />
|
||||
|
@ -1,14 +1,14 @@
|
||||
<div class="form-control">
|
||||
@if ($label)
|
||||
<label class="label">
|
||||
<span class="label-text">
|
||||
<label class="flex items-center gap-1 mb-1 text-sm font-medium">
|
||||
<span>
|
||||
@if ($label)
|
||||
{{ $label }}
|
||||
@else
|
||||
{{ $id }}
|
||||
@endif
|
||||
@if ($required)
|
||||
<span class="text-warning">*</span>
|
||||
<x-highlighted text="*" />
|
||||
@endif
|
||||
@if ($helper)
|
||||
<div class="group">
|
||||
|
1
resources/views/components/highlighted.blade.php
Normal file
1
resources/views/components/highlighted.blade.php
Normal file
@ -0,0 +1 @@
|
||||
<span class="inline-block text-warning">{{$text}}</span>
|
@ -26,7 +26,7 @@
|
||||
<body>
|
||||
@livewireScripts
|
||||
<x-toaster-hub />
|
||||
<main class="main max-w-screen-2xl">
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
<x-version class="fixed left-2 bottom-1" />
|
||||
|
@ -26,7 +26,7 @@
|
||||
<body>
|
||||
@livewireScripts
|
||||
<x-toaster-hub />
|
||||
@if (is_instance_admin() || is_subscription_in_grace_period())
|
||||
@if (isInstanceAdmin() || is_subscription_in_grace_period())
|
||||
<div class="fixed top-3 left-4" id="vue">
|
||||
<magic-bar></magic-bar>
|
||||
</div>
|
||||
|
@ -65,7 +65,7 @@ class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" vie
|
||||
</a>
|
||||
</li>
|
||||
<div class="flex-1"></div>
|
||||
@if (is_instance_admin())
|
||||
@if (isInstanceAdmin())
|
||||
<livewire:upgrade />
|
||||
@endif
|
||||
<li title="Profile">
|
||||
@ -80,7 +80,7 @@ class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" vie
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@if (is_instance_admin())
|
||||
@if (isInstanceAdmin())
|
||||
<li title="Settings" class="mt-auto">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('settings')) href="/settings" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<x-layout>
|
||||
<h1>Dashboard</h1>
|
||||
<div class="subtitle">Something <span class="text-warning">(more)</span> useful will be here.</div>
|
||||
<div class="subtitle">Something <x-highlighted text="(more)"/> useful will be here.</div>
|
||||
<div class="w-full rounded stats stats-vertical lg:stats-horizontal">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Servers</div>
|
||||
|
146
resources/views/livewire/boarding.blade.php
Normal file
146
resources/views/livewire/boarding.blade.php
Normal file
@ -0,0 +1,146 @@
|
||||
<div class="min-h-screen hero">
|
||||
<div class="hero-content">
|
||||
<div>
|
||||
@if ($currentState === 'welcome')
|
||||
<h1 class="text-5xl font-bold">Welcome to Coolify</h1>
|
||||
<p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
|
||||
<div class="flex justify-center ">
|
||||
<div class="w-72 box" wire:click="$set('currentState', 'select-server')">Get Started
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if ($currentState === 'select-server')
|
||||
<x-boarding-step title="Server">
|
||||
<x-slot:question>
|
||||
Do you want to deploy your resources on your <x-highlighted text="Localhost" />
|
||||
or on a <x-highlighted text="Remote Server" />?
|
||||
</x-slot:question>
|
||||
<x-slot:actions>
|
||||
<div class="md:w-72 box" wire:click="setServer('localhost')">Localhost
|
||||
</div>
|
||||
<div class="md:w-72 box" wire:click="setServer('remote')">Remote Server
|
||||
</div>
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>Servers are the main building blocks, as they will host your applications, databases,
|
||||
services, called resources. Any CPU intensive process will use the server's CPU where you
|
||||
are deploying your resources.</p>
|
||||
<p>Localhost is the server where Coolify is running on. It is not recommended to use one server
|
||||
for everyting.</p>
|
||||
<p>Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud
|
||||
provider.</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@endif
|
||||
@if ($currentState === 'private-key')
|
||||
<x-boarding-step title="Private Key">
|
||||
<x-slot:question>
|
||||
Do you have your own Private Key?
|
||||
</x-slot:question>
|
||||
<x-slot:actions>
|
||||
<div class="md:w-72 box" wire:click="setPrivateKey('own')">Yes
|
||||
</div>
|
||||
<div class="md:w-72 box" wire:click="setPrivateKey('create')">No (create one for me)
|
||||
</div>
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p>
|
||||
<p>You can use your own private key, or you can let Coolify to create one for you.</p>
|
||||
<p>In both ways, you need to add the public version of your private key to the remote server's
|
||||
<code>~/.ssh/authorized_keys</code> file.
|
||||
</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@endif
|
||||
@if ($currentState === 'create-private-key')
|
||||
<x-boarding-step title="Create Private Key">
|
||||
<x-slot:question>
|
||||
Please let me know your key details.
|
||||
</x-slot:question>
|
||||
<x-slot:actions>
|
||||
<form wire:submit.prevent='savePrivateKey' class="flex flex-col w-full gap-4 pr-10">
|
||||
<x-forms.input required placeholder="Choose a name for your Private Key. Could be anything."
|
||||
label="Name" id="privateKeyName" />
|
||||
<x-forms.input placeholder="Description, so others will know more about this."
|
||||
label="Description" id="privateKeyDescription" />
|
||||
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
label="Private Key" id="privateKey" />
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
</form>
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p>
|
||||
<p>You can use your own private key, or you can let Coolify to create one for you.</p>
|
||||
<p>In both ways, you need to add the public version of your private key to the remote server's
|
||||
<code>~/.ssh/authorized_keys</code> file.
|
||||
</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@endif
|
||||
@if ($currentState === 'create-server')
|
||||
<x-boarding-step title="Create Server">
|
||||
<x-slot:question>
|
||||
Please let me know your server details.
|
||||
</x-slot:question>
|
||||
<x-slot:actions>
|
||||
<form wire:submit.prevent='saveServer' class="flex flex-col w-full gap-4 pr-10">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input required placeholder="Choose a name for your Server. Could be anything."
|
||||
label="Name" id="remoteServerName" />
|
||||
<x-forms.input placeholder="Description, so others will know more about this."
|
||||
label="Description" id="remoteServerDescription" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input required placeholder="Hostname or IP address"
|
||||
label="Hostname or IP Address" id="remoteServerHost" />
|
||||
<x-forms.input required placeholder="Port number of your server. Default is 22."
|
||||
label="Port" id="remoteServerPort" />
|
||||
<x-forms.input required readonly
|
||||
placeholder="Username to connect to your server. Default is root." label="Username"
|
||||
id="remoteServerUser" />
|
||||
</div>
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
</form>
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>Username should be <x-highlighted text="root" /> for now. We are working on to use
|
||||
non-root users.</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@endif
|
||||
@if ($currentState === 'install-docker')
|
||||
<x-boarding-step title="Install Docker">
|
||||
<x-slot:question>
|
||||
Could not find Docker Engine on your server. Do you want me to install it for you?
|
||||
</x-slot:question>
|
||||
<x-slot:actions>
|
||||
<div class="w-72 box" wire:click="installDocker">Let's do it!</div>
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>This will install the latest Docker Engine on your server, configure a few things to be able to run optimal.</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@endif
|
||||
@if ($currentState === 'create-project')
|
||||
<x-boarding-step title="Project">
|
||||
<x-slot:question>
|
||||
I will create an initial project for you. You can change all the details later on.
|
||||
</x-slot:question>
|
||||
<x-slot:actions>
|
||||
<div class="w-72 box" wire:click="createNewProject">Let's do it!</div>
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>Projects are bound together several resources into one virtual group. There are no
|
||||
limitations on the number of projects you could have.</p>
|
||||
<p>Each project should have at least one environment. This helps you to create a production &
|
||||
staging version of the same application, but grouped separately.</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@endif
|
||||
<div class="flex justify-center gap-2 pt-4">
|
||||
<a wire:click='skipBoarding'>Skip boarding process</a>
|
||||
<a wire:click='restartBoarding'>Restart boarding process</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -25,7 +25,7 @@
|
||||
@foreach ($networks as $network)
|
||||
<a
|
||||
href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}">
|
||||
<x-forms.button>Add<span class="text-warning">{{ data_get($network, 'Name') }}</span>
|
||||
<x-forms.button>+<x-highlighted text="{{ data_get($network, 'Name') }}" />
|
||||
</x-forms.button>
|
||||
</a>
|
||||
@endforeach
|
||||
|
@ -16,7 +16,7 @@
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
@if (is_instance_admin())
|
||||
@if (isInstanceAdmin())
|
||||
<x-forms.button wire:click='copyFromInstanceSettings'>
|
||||
Copy from Instance Settings
|
||||
</x-forms.button>
|
||||
|
@ -9,9 +9,8 @@
|
||||
</x-modal>
|
||||
<x-modal yesOrNo modalId="startProxy" modalTitle="Start Proxy" action="start_proxy">
|
||||
<x-slot:modalBody>
|
||||
<p>This will start the proxy on this server and <span class="text-warning">stop any running process that is
|
||||
using port 80 and
|
||||
443</span>.
|
||||
<p>This will start the proxy on this server and
|
||||
<x-highlighted text="stop any running process that is using port 80 and 443" />.
|
||||
<br>Please think
|
||||
again.
|
||||
</p>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div>
|
||||
<div>Status: {{ auth()->user()->currentTeam()->subscription->lemon_status }}</div>
|
||||
<div>Type: {{ auth()->user()->currentTeam()->subscription->lemon_variant_name }}</div>
|
||||
@if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<div>Status: {{ currentTeam()->subscription->lemon_status }}</div>
|
||||
<div>Type: {{ currentTeam()->subscription->lemon_variant_name }}</div>
|
||||
@if (currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div>
|
||||
<div class="py-4">If you would like to change the subscription to a lower/higher plan, <a
|
||||
class="text-white underline" href="https://docs.coollabs.io/contact" target="_blank">please
|
||||
@ -12,7 +12,7 @@ class="text-white underline" href="https://docs.coollabs.io/contact" target="_bl
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
@if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
@if (currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<x-forms.button class="bg-coollabs-gradient" wire:click='resume'>Resume Subscription
|
||||
</x-forms.button>
|
||||
@else
|
||||
|
@ -11,11 +11,11 @@
|
||||
<div>This is the default team. You can't delete it.</div>
|
||||
@elseif(auth()->user()->teams()->get()->count() === 1)
|
||||
<div>You can't delete your last team.</div>
|
||||
@elseif(auth()->user()->currentTeam()->subscription &&
|
||||
auth()->user()->currentTeam()->subscription?->lemon_status !== 'cancelled')
|
||||
@elseif(currentTeam()->subscription &&
|
||||
currentTeam()->subscription?->lemon_status !== 'cancelled')
|
||||
<div>Please cancel your subscription before delete this team (Manage My Subscription button).</div>
|
||||
@else
|
||||
@if (auth()->user()->currentTeam()->isEmpty())
|
||||
@if (currentTeam()->isEmpty())
|
||||
<div class="pb-4">This will delete your team. Beware! There is no coming back!</div>
|
||||
<x-forms.button isError isModal modalId="deleteTeam">
|
||||
Delete
|
||||
@ -25,25 +25,25 @@
|
||||
<div class="pb-4">You need to delete the following resources to be able to delete the team:</div>
|
||||
<h4 class="pb-4">Projects:</h4>
|
||||
<ul class="pl-8 list-disc">
|
||||
@foreach (auth()->user()->currentTeam()->projects as $resource)
|
||||
@foreach (currentTeam()->projects as $resource)
|
||||
<li>{{ $resource->name }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<h4 class="py-4">Servers:</h4>
|
||||
<ul class="pl-8 list-disc">
|
||||
@foreach (auth()->user()->currentTeam()->servers as $resource)
|
||||
@foreach (currentTeam()->servers as $resource)
|
||||
<li>{{ $resource->name }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<h4 class="py-4">Private Keys:</h4>
|
||||
<ul class="pl-8 list-disc">
|
||||
@foreach (auth()->user()->currentTeam()->privateKeys as $resource)
|
||||
@foreach (currentTeam()->privateKeys as $resource)
|
||||
<li>{{ $resource->name }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<h4 class="py-4">Sources:</h4>
|
||||
<ul class="pl-8 list-disc">
|
||||
@foreach (auth()->user()->currentTeam()->sources() as $resource)
|
||||
@foreach (currentTeam()->sources() as $resource)
|
||||
<li>{{ $resource->name }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
@ -14,7 +14,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (auth()->user()->currentTeam()->members->sortBy('name') as $member)
|
||||
@foreach (currentTeam()->members->sortBy('name') as $member)
|
||||
<livewire:team.member :member="$member" :wire:key="$member->id" />
|
||||
@endforeach
|
||||
</tbody>
|
||||
@ -26,7 +26,7 @@
|
||||
<h3 class="pb-4">Invite a new member</h3>
|
||||
@else
|
||||
<h3>Invite a new member</h3>
|
||||
@if (is_instance_admin())
|
||||
@if (isInstanceAdmin())
|
||||
<div class="pb-4 text-xs text-warning">You need to configure <a href="/settings/emails"
|
||||
class="underline text-warning">Transactional
|
||||
Emails</a>
|
||||
|
@ -6,7 +6,7 @@
|
||||
@if (is_cloud())
|
||||
<div class="pb-8">
|
||||
<h2>Subscription</h2>
|
||||
@if (data_get(auth()->user()->currentTeam(),
|
||||
@if (data_get(currentTeam(),
|
||||
'subscription'))
|
||||
<livewire:subscription.actions />
|
||||
@else
|
||||
|
@ -93,6 +93,7 @@
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/', [Controller::class, 'dashboard'])->name('dashboard');
|
||||
Route::get('/boarding', [Controller::class, 'boarding'])->name('boarding');
|
||||
Route::middleware(['throttle:force-password-reset'])->group(function() {
|
||||
Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset');
|
||||
});
|
||||
@ -126,7 +127,7 @@
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/source/new', fn () => view('source.new'))->name('source.new');
|
||||
Route::get('/sources', function () {
|
||||
$sources = auth()->user()->currentTeam()->sources();
|
||||
$sources = currentTeam()->sources();
|
||||
return view('source.all', [
|
||||
'sources' => $sources,
|
||||
]);
|
||||
|
Loading…
Reference in New Issue
Block a user