Compare commits

...

18 Commits
1.1.0 ... 1.2.1

Author SHA1 Message Date
e9016737d4 build frontend 2024-04-05 19:49:35 +02:00
f34d5eb82b Bump vite from 4.5.2 to 4.5.3 (#152) 2024-04-05 19:48:37 +02:00
12c500e125 Bug fixes (#155) 2024-04-05 19:45:09 +02:00
2d566b853f use textarea for code editor (#151) 2024-04-03 22:38:28 +02:00
ca93b521ec Merge branch 'main' into 1.x 2024-04-01 21:19:14 +02:00
bce05d3171 Merge pull request #148 from vitodeploy/versioning
show current version
2024-04-01 20:50:03 +02:00
929dd1dbaa show version a bit trasparent on mobile 2024-04-01 00:06:29 +02:00
2bcd145bea docker 2024-03-31 23:58:45 +02:00
c0f903d4ca show current version 2024-03-31 23:29:22 +02:00
cca4ab7ae3 fix code editor 2024-03-29 18:40:20 +01:00
51e7325d3d fix trusted procies 2024-03-29 18:25:14 +01:00
ce085879c1 Merge pull request #144 from vitodeploy/fix-env-update
empty content on editing file
2024-03-29 12:29:21 +01:00
8a49003e9e fix focus issue 2024-03-29 12:21:33 +01:00
dcc4276f09 fix spacing in the editor 2024-03-29 10:07:14 +01:00
f089779045 empty content on editing file 2024-03-29 00:42:36 +01:00
f1efb9a6c8 make project dropdown full width #132 2024-03-28 18:59:37 +01:00
a7d472fb45 update discord link 2024-03-27 22:33:24 +01:00
a0af4e3e9d Merge pull request #128 from vitodeploy/1.x
Merge
2024-03-24 10:07:20 +01:00
56 changed files with 398 additions and 187 deletions

View File

@ -12,5 +12,5 @@ MAIL_PORT=
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"

View File

@ -12,5 +12,5 @@ MAIL_PORT=
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"

View File

@ -13,5 +13,5 @@ MAIL_PORT=
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"

2
.gitignore vendored
View File

@ -7,6 +7,8 @@
/storage/test-key /storage/test-key
/storage/test-key.pub /storage/test-key.pub
/vendor /vendor
/storage/database.sqlite
/storage/database-test.sqlite
.env .env
.env.backup .env.backup
.env.production .env.production

View File

@ -37,7 +37,7 @@ ## Useful Links
- [Feedbacks](https://vitodeploy.featurebase.app) - [Feedbacks](https://vitodeploy.featurebase.app)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap) - [Roadmap](https://vitodeploy.featurebase.app/roadmap)
- [Video Demo](https://youtu.be/rLRHIyEfON8) - [Video Demo](https://youtu.be/rLRHIyEfON8)
- [Discord](https://discord.gg/dcUWA5DV) - [Discord](https://discord.gg/uZeeHZZnm5)
- [Contribution](/CONTRIBUTING.md) - [Contribution](/CONTRIBUTING.md)
- [Security](/SECURITY.md) - [Security](/SECURITY.md)

View File

@ -34,8 +34,6 @@ public function create(Site $site, array $input): void
$ssl->status = SslStatus::CREATED; $ssl->status = SslStatus::CREATED;
$ssl->save(); $ssl->save();
$site->type()->edit(); $site->type()->edit();
})->catch(function () use ($ssl) {
$ssl->delete();
}); });
} }

View File

@ -3,6 +3,8 @@
namespace App\Actions\Site; namespace App\Actions\Site;
use App\Enums\SiteStatus; use App\Enums\SiteStatus;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier; use App\Facades\Notifier;
use App\Models\Server; use App\Models\Server;
@ -19,7 +21,6 @@
class CreateSite class CreateSite
{ {
/** /**
* @throws SourceControlIsNotConnected
* @throws ValidationException * @throws ValidationException
*/ */
public function create(Server $server, array $input): Site public function create(Server $server, array $input): Site
@ -47,7 +48,15 @@ public function create(Server $server, array $input): Site
} }
} catch (SourceControlIsNotConnected) { } catch (SourceControlIsNotConnected) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'source_control' => __('Source control is not connected'), 'source_control' => 'Source control is not connected',
]);
} catch (RepositoryPermissionDenied) {
throw ValidationException::withMessages([
'repository' => 'You do not have permission to access this repository',
]);
} catch (RepositoryNotFound) {
throw ValidationException::withMessages([
'repository' => 'Repository not found',
]); ]);
} }

View File

@ -0,0 +1,49 @@
<?php
namespace App\Actions\Site;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class UpdateSourceControl
{
public function update(Site $site, array $input): void
{
$this->validate($input);
$site->source_control_id = $input['source_control'];
try {
if ($site->sourceControl()) {
$site->sourceControl()->getRepo($site->repository);
}
} catch (SourceControlIsNotConnected) {
throw ValidationException::withMessages([
'source_control' => 'Source control is not connected',
]);
} catch (RepositoryPermissionDenied) {
throw ValidationException::withMessages([
'repository' => 'You do not have permission to access this repository',
]);
} catch (RepositoryNotFound) {
throw ValidationException::withMessages([
'repository' => 'Repository not found',
]);
}
$site->save();
}
private function validate(array $input): void
{
Validator::make($input, [
'source_control' => [
'required',
Rule::exists('source_controls', 'id'),
],
])->validate();
}
}

View File

@ -2,9 +2,7 @@
namespace App\Exceptions; namespace App\Exceptions;
use Exception; class SSHAuthenticationError extends SSHError
class SSHAuthenticationError extends Exception
{ {
// //
} }

View File

@ -2,9 +2,7 @@
namespace App\Exceptions; namespace App\Exceptions;
use Exception; class SSLCreationException extends SSHError
class SSLCreationException extends Exception
{ {
// //
} }

View File

@ -133,7 +133,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
$this->log?->write($output); $this->log?->write($output);
if (Str::contains($output, 'VITO_SSH_ERROR')) { if (Str::contains($output, 'VITO_SSH_ERROR')) {
throw new Exception('SSH command failed with an error'); throw new SSHCommandError('SSH command failed with an error');
} }
return $output; return $output;

View File

@ -1,10 +1,11 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\API;
use App\Actions\Site\Deploy; use App\Actions\Site\Deploy;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier; use App\Facades\Notifier;
use App\Http\Controllers\Controller;
use App\Models\GitHook; use App\Models\GitHook;
use App\Models\ServerLog; use App\Models\ServerLog;
use App\Notifications\SourceControlDisconnected; use App\Notifications\SourceControlDisconnected;

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
class HealthController extends Controller
{
public function __invoke()
{
return response()->json([
'success' => true,
'version' => vito_version(),
]);
}
}

View File

@ -7,6 +7,8 @@
use App\Actions\Site\UpdateDeploymentScript; use App\Actions\Site\UpdateDeploymentScript;
use App\Actions\Site\UpdateEnv; use App\Actions\Site\UpdateEnv;
use App\Exceptions\DeploymentScriptIsEmptyException; use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
@ -24,12 +26,14 @@ public function deploy(Server $server, Site $site): HtmxResponse
app(Deploy::class)->run($site); app(Deploy::class)->run($site);
Toast::success('Deployment started!'); Toast::success('Deployment started!');
} catch (SourceControlIsNotConnected $e) { } catch (SourceControlIsNotConnected) {
Toast::error($e->getMessage()); Toast::error('Source control is not connected. Check site\'s settings.');
return htmx()->redirect(route('source-controls'));
} catch (DeploymentScriptIsEmptyException) { } catch (DeploymentScriptIsEmptyException) {
Toast::error('Deployment script is empty!'); Toast::error('Deployment script is empty!');
} catch (RepositoryPermissionDenied) {
Toast::error('You do not have permission to access this repository!');
} catch (RepositoryNotFound) {
Toast::error('Repository not found!');
} }
return htmx()->back(); return htmx()->back();
@ -83,6 +87,12 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
Toast::success('Auto deployment has been enabled.'); Toast::success('Auto deployment has been enabled.');
} catch (SourceControlIsNotConnected) { } catch (SourceControlIsNotConnected) {
Toast::error('Source control is not connected. Check site\'s settings.'); Toast::error('Source control is not connected. Check site\'s settings.');
} catch (DeploymentScriptIsEmptyException) {
Toast::error('Deployment script is empty!');
} catch (RepositoryPermissionDenied) {
Toast::error('You do not have permission to access this repository!');
} catch (RepositoryNotFound) {
Toast::error('Repository not found!');
} }
} }

View File

@ -4,6 +4,7 @@
use App\Actions\Site\CreateSite; use App\Actions\Site\CreateSite;
use App\Actions\Site\DeleteSite; use App\Actions\Site\DeleteSite;
use App\Enums\SiteStatus;
use App\Enums\SiteType; use App\Enums\SiteType;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
@ -42,14 +43,38 @@ public function create(Server $server): View
]); ]);
} }
public function show(Server $server, Site $site): View public function show(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{ {
if (in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.installing', [$server, $site]));
}
return redirect()->route('servers.sites.installing', [$server, $site]);
}
return view('sites.show', [ return view('sites.show', [
'server' => $server, 'server' => $server,
'site' => $site, 'site' => $site,
]); ]);
} }
public function installing(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{
if (! in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.show', [$server, $site]));
}
return redirect()->route('servers.sites.show', [$server, $site]);
}
return view('sites.installing', [
'server' => $server,
'site' => $site,
]);
}
public function destroy(Server $server, Site $site): RedirectResponse public function destroy(Server $server, Site $site): RedirectResponse
{ {
app(DeleteSite::class)->delete($site); app(DeleteSite::class)->delete($site);

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Site\UpdateSourceControl;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Models\Server; use App\Models\Server;
@ -63,4 +64,13 @@ public function updatePHPVersion(Server $server, Site $site, Request $request):
return htmx()->back(); return htmx()->back();
} }
public function updateSourceControl(Server $server, Site $site, Request $request): HtmxResponse
{
$site = app(UpdateSourceControl::class)->update($site, $request->input());
Toast::success('Source control updated successfully!');
return htmx()->back();
}
} }

View File

@ -12,7 +12,7 @@ class TrustProxies extends Middleware
* *
* @var array<int, string>|string|null * @var array<int, string>|string|null
*/ */
protected $proxies; protected $proxies = '*';
/** /**
* The headers that should be used to detect proxies. * The headers that should be used to detect proxies.
@ -20,7 +20,7 @@ class TrustProxies extends Middleware
* @var int * @var int
*/ */
protected $headers = protected $headers =
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PROTO |

View File

@ -7,7 +7,6 @@
use App\Helpers\Toast; use App\Helpers\Toast;
use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -37,9 +36,5 @@ public function boot(): void
$this->app->bind('toast', function () { $this->app->bind('toast', function () {
return new Toast; return new Toast;
}); });
if (str(config('app.url'))->startsWith('https://')) {
URL::forceScheme('https');
}
} }
} }

View File

@ -98,12 +98,12 @@ public function reboot(): void
); );
} }
public function editFile(string $path, string $content): void public function editFile(string $path, ?string $content = null): void
{ {
$this->server->ssh()->exec( $this->server->ssh()->exec(
$this->getScript('edit-file.sh', [ $this->getScript('edit-file.sh', [
'path' => $path, 'path' => $path,
'content' => $content, 'content' => $content ?? '',
]), ]),
); );
} }

View File

@ -29,3 +29,8 @@ function htmx(): HtmxResponse
{ {
return new HtmxResponse(); return new HtmxResponse();
} }
function vito_version(): string
{
return exec('git describe --tags');
}

View File

@ -27,9 +27,9 @@ COPY docker/php.ini /etc/php/8.2/cli/conf.d/99-vito.ini
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# app # app
COPY . /var/www/html RUN rm -rf /var/www/html
RUN rm -rf /var/www/html/vendor RUN git clone -b 1.x https://github.com/vitodeploy/vito.git /var/www/html
RUN rm -rf /var/www/html/.env RUN git checkout $(git tag -l --merged 1.x --sort=-v:refname | head -n 1)
RUN composer install --no-dev --prefer-dist RUN composer install --no-dev --prefer-dist
RUN chown -R www-data:www-data /var/www/html \ RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache && chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache

14
package-lock.json generated
View File

@ -20,7 +20,7 @@
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"toastr": "^2.1.4", "toastr": "^2.1.4",
"vite": "^4.5.2" "vite": "^4.5.3"
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
@ -1812,9 +1812,9 @@
"dev": true "dev": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.2", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
@ -3011,9 +3011,9 @@
"dev": true "dev": true
}, },
"vite": { "vite": {
"version": "4.5.2", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",

View File

@ -22,6 +22,6 @@
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"toastr": "^2.1.4", "toastr": "^2.1.4",
"vite": "^4.5.2" "vite": "^4.5.3"
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"resources/css/app.css": { "resources/css/app.css": {
"file": "assets/app-eff15392.css", "file": "assets/app-e3775b0a.css",
"isEntry": true, "isEntry": true,
"src": "resources/css/app.css" "src": "resources/css/app.css"
}, },
@ -12,7 +12,7 @@
"css": [ "css": [
"assets/app-a1ae07b3.css" "assets/app-a1ae07b3.css"
], ],
"file": "assets/app-a74f846c.js", "file": "assets/app-4c0bf3b2.js",
"isEntry": true, "isEntry": true,
"src": "resources/js/app.js" "src": "resources/js/app.js"
} }

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
ace.define("ace/mode/sh", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text", "ace/tokenizer", "ace/mode/sh_highlight_rules", "ace/range"], function (e, t, n) { var r = e("../lib/oop"), i = e("./text").Mode, s = e("../tokenizer").Tokenizer, o = e("./sh_highlight_rules").ShHighlightRules, u = e("../range").Range, a = function () { this.$tokenizer = new s((new o).getRules()) }; r.inherits(a, i), function () { this.toggleCommentLines = function (e, t, n, r) { var i = !0, s = /^(\s*)#/; for (var o = n; o <= r; o++)if (!s.test(t.getLine(o))) { i = !1; break } if (i) { var a = new u(0, 0, 0, 0); for (var o = n; o <= r; o++) { var f = t.getLine(o), l = f.match(s); a.start.row = o, a.end.row = o, a.end.column = l[0].length, t.replace(a, l[1]) } } else t.indentRows(n, r, "#") }, this.getNextLineIndent = function (e, t, n) { var r = this.$getIndent(t), i = this.$tokenizer.getLineTokens(t, e), s = i.tokens; if (s.length && s[s.length - 1].type == "comment") return r; if (e == "start") { var o = t.match(/^.*[\{\(\[\:]\s*$/); o && (r += n) } return r }; var e = { pass: 1, "return": 1, raise: 1, "break": 1, "continue": 1 }; this.checkOutdent = function (t, n, r) { if (r !== "\r\n" && r !== "\r" && r !== "\n") return !1; var i = this.$tokenizer.getLineTokens(n.trim(), t).tokens; if (!i) return !1; do var s = i.pop(); while (s && (s.type == "comment" || s.type == "text" && s.value.match(/^\s+$/))); return s ? s.type == "keyword" && e[s.value] : !1 }, this.autoOutdent = function (e, t, n) { n += 1; var r = this.$getIndent(t.getLine(n)), i = t.getTabString(); r.slice(-i.length) == i && t.remove(new u(n, r.length - i.length, n, r.length)) } }.call(a.prototype), t.Mode = a }), ace.define("ace/mode/sh_highlight_rules", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text_highlight_rules"], function (e, t, n) { var r = e("../lib/oop"), i = e("./text_highlight_rules").TextHighlightRules, s = t.reservedKeywords = "!|{|}|case|do|done|elif|else|esac|fi|for|if|in|then|until|while|&|;|export|local|read|typeset|unset|elif|select|set", o = t.languageConstructs = "[|]|alias|bg|bind|break|builtin|cd|command|compgen|complete|continue|dirs|disown|echo|enable|eval|exec|exit|fc|fg|getopts|hash|help|history|jobs|kill|let|logout|popd|printf|pushd|pwd|return|set|shift|shopt|source|suspend|test|times|trap|type|ulimit|umask|unalias|wait", u = function () { var e = this.createKeywordMapper({ keyword: s, "support.function.builtin": o, "invalid.deprecated": "debugger" }, "identifier"), t = "(?:(?:[1-9]\\d*)|(?:0))", n = "(?:\\.\\d+)", r = "(?:\\d+)", i = "(?:(?:" + r + "?" + n + ")|(?:" + r + "\\.))", u = "(?:(?:" + i + "|" + r + ")" + ")", a = "(?:" + u + "|" + i + ")", f = "(?:&" + r + ")", l = "[a-zA-Z][a-zA-Z0-9_]*", c = "(?:(?:\\$" + l + ")|(?:" + l + "=))", h = "(?:\\$(?:SHLVL|\\$|\\!|\\?))", p = "(?:" + l + "\\s*\\(\\))"; this.$rules = { start: [{ token: "comment", regex: "#.*$" }, { token: "string", regex: '"(?:[^\\\\]|\\\\.)*?"' }, { token: "variable.language", regex: h }, { token: "variable", regex: c }, { token: "support.function", regex: p }, { token: "support.function", regex: f }, { token: "string", regex: "'(?:[^\\\\]|\\\\.)*?'" }, { token: "constant.numeric", regex: a }, { token: "constant.numeric", regex: t + "\\b" }, { token: e, regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" }, { token: "keyword.operator", regex: "\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|~|<|>|<=|=>|=|!=" }, { token: "paren.lparen", regex: "[\\[\\(\\{]" }, { token: "paren.rparen", regex: "[\\]\\)\\}]" }, { token: "text", regex: "\\s+" }] } }; r.inherits(u, i), t.ShHighlightRules = u })

View File

@ -1,5 +0,0 @@
ace.define("ace/theme/github", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) {
t.isDark = !1, t.cssClass = "ace-github", t.cssText = '/* CSS style content from github\'s default pygments highlighter template.Cursor and selection styles from textmate.css. */.ace-github .ace_gutter {background: #e8e8e8;color: #AAA;}.ace-github .ace_scroller {background: #fff;}.ace-github .ace_keyword {font-weight: bold;}.ace-github .ace_string {color: #D14;}.ace-github .ace_variable.ace_class {color: teal;}.ace-github .ace_constant.ace_numeric {color: #099;}.ace-github .ace_constant.ace_buildin {color: #0086B3;}.ace-github .ace_support.ace_function {color: #0086B3;}.ace-github .ace_comment {color: #998;font-style: italic;}.ace-github .ace_variable.ace_language {color: #0086B3;}.ace-github .ace_paren {font-weight: bold;}.ace-github .ace_boolean {font-weight: bold;}.ace-github .ace_string.ace_regexp {color: #009926;font-weight: normal;}.ace-github .ace_variable.ace_instance {color: teal;}.ace-github .ace_constant.ace_language {font-weight: bold;}.ace-github .ace_text-layer {}.ace-github .ace_cursor {border-left: 2px solid black;}.ace-github .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid black;}.ace-github .ace_marker-layer .ace_active-line {background: rgb(255, 255, 204);}.ace-github .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-github.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px white;border-radius: 2px;}/* bold keywords cause cursor issues for some fonts *//* this disables bold style for editor and keeps for static highlighter */.ace-github.ace_nobold .ace_line > span {font-weight: normal !important;}.ace-github .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-github .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-github .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-github .ace_gutter-active-line {background-color : rgba(0, 0, 0, 0.07);}.ace-github .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-github .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-github .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';
var r = e("../lib/dom");
r.importCssString(t.cssText, t.cssClass)
})

View File

@ -1,5 +0,0 @@
ace.define("ace/theme/one-dark", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) {
t.isDark = !1, t.cssClass = "ace-one-dark", t.cssText = '/* CSS style content from one-dark\'s default pygments highlighter template.Cursor and selection styles from textmate.css. */.ace-one-dark .ace_gutter{background:#282c34;color:#6a6f7a}.ace-one-dark .ace_print-margin{width:1px;background:#e8e8e8}.ace-one-dark{background-color:#282c34;color:#abb2bf}.ace-one-dark .ace_cursor{color:#528bff}.ace-one-dark .ace_marker-layer .ace_selection{background:#3d4350}.ace-one-dark.ace_multiselect .ace_selection.ace_start{box-shadow:0 0 3px 0 #282c34;border-radius:2px}.ace-one-dark .ace_marker-layer .ace_step{background:#c6dbae}.ace-one-dark .ace_marker-layer .ace_bracket{margin:-1px 0 0 -1px;border:1px solid #747369}.ace-one-dark .ace_marker-layer .ace_active-line{background:rgba(76,87,103,.19)}.ace-one-dark .ace_gutter-active-line{background-color:rgba(76,87,103,.19)}.ace-one-dark .ace_marker-layer .ace_selected-word{border:1px solid #3d4350}.ace-one-dark .ace_fold{background-color:#61afef;border-color:#abb2bf}.ace-one-dark .ace_keyword{color:#c678dd}.ace-one-dark .ace_keyword.ace_operator{color:#c678dd}.ace-one-dark .ace_keyword.ace_other.ace_unit{color:#d19a66}.ace-one-dark .ace_constant.ace_language{color:#d19a66}.ace-one-dark .ace_constant.ace_numeric{color:#d19a66}.ace-one-dark .ace_constant.ace_character{color:#56b6c2}.ace-one-dark .ace_constant.ace_other{color:#56b6c2}.ace-one-dark .ace_support.ace_function{color:#61afef}.ace-one-dark .ace_support.ace_constant{color:#d19a66}.ace-one-dark .ace_support.ace_class{color:#e5c07b}.ace-one-dark .ace_support.ace_type{color:#e5c07b}.ace-one-dark .ace_storage{color:#c678dd}.ace-one-dark .ace_storage.ace_type{color:#c678dd}.ace-one-dark .ace_invalid{color:#fff;background-color:#f2777a}.ace-one-dark .ace_invalid.ace_deprecated{color:#272b33;background-color:#d27b53}.ace-one-dark .ace_string{color:#98c379}.ace-one-dark .ace_string.ace_regexp{color:#e06c75}.ace-one-dark .ace_comment{font-style:italic;color:#5c6370}.ace-one-dark .ace_variable{color:#e06c75}.ace-one-dark .ace_variable.ace_parameter{color:#d19a66}.ace-one-dark .ace_meta.ace_tag{color:#e06c75}.ace-one-dark .ace_entity.ace_other.ace_attribute-name{color:#e06c75}.ace-one-dark .ace_entity.ace_name.ace_function{color:#61afef}.ace-one-dark .ace_entity.ace_name.ace_tag{color:#e06c75}.ace-one-dark .ace_markup.ace_heading{color:#98c379}.ace-one-dark .ace_indent-guide{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ09NrYAgMjP4PAAtGAwchHMyAAAAAAElFTkSuQmCC) right repeat-y}';
var r = e("../lib/dom");
r.importCssString(t.cssText, t.cssClass)
})

View File

@ -23,8 +23,8 @@ window.htmx.defineExtension('disable-element', {
}); });
document.body.addEventListener('htmx:configRequest', (event) => { document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRF-TOKEN'] = document.head.querySelector('meta[name="csrf-token"]').content; event.detail.headers['X-CSRF-TOKEN'] = document.head.querySelector('meta[name="csrf-token"]').content;
if (window.getSelection) { window.getSelection().removeAllRanges(); } // if (window.getSelection) { window.getSelection().removeAllRanges(); }
else if (document.selection) { document.selection.empty(); } // else if (document.selection) { document.selection.empty(); }
}); });
document.body.addEventListener('htmx:beforeRequest', (event) => { document.body.addEventListener('htmx:beforeRequest', (event) => {
let targetElements = event.target.querySelectorAll('[hx-disable]'); let targetElements = event.target.querySelectorAll('[hx-disable]');
@ -38,6 +38,13 @@ document.body.addEventListener('htmx:afterRequest', (event) => {
targetElements[i].disabled = false; targetElements[i].disabled = false;
} }
}); });
document.body.addEventListener('htmx:afterSwap', (event) => {
tippy('[data-tooltip]', {
content(reference) {
return reference.getAttribute('data-tooltip');
},
});
});
import toastr from 'toastr'; import toastr from 'toastr';
window.toastr = toastr; window.toastr = toastr;
@ -49,13 +56,6 @@ window.toastr.options = {
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css'; import 'tippy.js/dist/tippy.css';
document.body.addEventListener('htmx:afterSettle', (event) => {
tippy('[data-tooltip]', {
content(reference) {
return reference.getAttribute('data-tooltip');
},
});
});
tippy('[data-tooltip]', { tippy('[data-tooltip]', {
content(reference) { content(reference) {
return reference.getAttribute('data-tooltip'); return reference.getAttribute('data-tooltip');

View File

@ -4,6 +4,7 @@
<x-slot name="trigger"> <x-slot name="trigger">
<x-secondary-button> <x-secondary-button>
{{ __("Auto Deployment") }} {{ __("Auto Deployment") }}
<x-heroicon name="o-chevron-down" class="ml-1 h-4 w-4" />
</x-secondary-button> </x-secondary-button>
</x-slot> </x-slot>
<x-slot name="content"> <x-slot name="content">

View File

@ -12,11 +12,13 @@ class="p-6"
{{ __("Deployment Script") }} {{ __("Deployment Script") }}
</h2> </h2>
<div class="mt-6">A bash script that will be executed when you run the deployment process.</div>
<div class="mt-6"> <div class="mt-6">
<x-input-label for="script" :value="__('Script')" /> <x-input-label for="script" :value="__('Script')" />
<x-code-editor id="script" name="script" lang="sh" class="mt-1 w-full"> <x-textarea id="script" name="script" class="mt-1 min-h-[400px] w-full">
{{ old("script", $site->deploymentScript?->content) }} {{ old("script", $site->deploymentScript?->content) }}
</x-code-editor> </x-textarea>
@error("script") @error("script")
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />
@enderror @enderror

View File

@ -21,9 +21,9 @@ class="mt-6"
> >
<x-input-label for="env" :value="__('.env')" /> <x-input-label for="env" :value="__('.env')" />
<div id="env-content"> <div id="env-content">
<x-code-editor id="env" name="env" rows="10" class="mt-1 block w-full"> <x-textarea id="env" name="env" rows="10" class="mt-1 block min-h-[400px] w-full">
{{ old("env", session()->get("env") ?? "Loading...") }} {{ old("env", session()->get("env") ?? "Loading...") }}
</x-code-editor> </x-textarea>
</div> </div>
@error("env") @error("env")
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />

View File

@ -19,6 +19,7 @@
<x-slot name="trigger"> <x-slot name="trigger">
<x-secondary-button> <x-secondary-button>
{{ __("Manage") }} {{ __("Manage") }}
<x-heroicon name="o-chevron-down" class="ml-1 h-4 w-4" />
</x-secondary-button> </x-secondary-button>
</x-slot> </x-slot>
<x-slot name="content"> <x-slot name="content">

View File

@ -1,45 +0,0 @@
@props([
"id",
"name",
"disabled" => false,
"lang" => "text",
])
<div
x-data="{
editorId: @js($id),
disabled: @js($disabled),
lang: @js($lang),
init() {
document.body.addEventListener('htmx:afterSettle', (event) => {
let editor = null
let theme =
document.documentElement.className === 'dark'
? 'one-dark'
: 'github'
editor = window.ace.edit(this.editorId)
let contentElement = document.getElementById(
`text-${this.editorId}`,
)
editor.setValue(contentElement.innerText, 1)
if (this.disabled) {
editor.setReadOnly(true)
}
editor.getSession().setMode(`ace/mode/${this.lang}`)
editor.setTheme(`ace/theme/${theme}`)
editor.setFontSize('15px')
editor.setShowPrintMargin(false)
editor.on('change', () => {
contentElement.innerHTML = editor.getValue()
})
document.body.addEventListener('color-scheme-changed', (event) => {
theme = event.detail.theme === 'dark' ? 'one-dark' : 'github'
editor.setTheme(`ace/theme/${theme}`)
})
})
},
}"
>
<div id="{{ $id }}" class="min-h-[400px] w-full rounded-md border border-gray-200 dark:border-gray-700"></div>
<textarea id="text-{{ $id }}" name="{{ $name }}" class="hidden">{{ $slot }}</textarea>
</div>

View File

@ -10,7 +10,7 @@
runUrl: '{{ route("servers.console.run", ["server" => $server]) }}', runUrl: '{{ route("servers.console.run", ["server" => $server]) }}',
async run() { async run() {
this.running = true this.running = true
this.output = 'Running...\n' this.output = this.command + '\n'
const fetchOptions = { const fetchOptions = {
method: 'POST', method: 'POST',
headers: { headers: {
@ -23,6 +23,7 @@
}), }),
} }
this.command = ''
const response = await fetch(this.runUrl, fetchOptions) const response = await fetch(this.runUrl, fetchOptions)
const reader = response.body.getReader() const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8') const decoder = new TextDecoder('utf-8')

View File

@ -21,8 +21,6 @@
<link rel="preconnect" href="https://fonts.bunny.net" /> <link rel="preconnect" href="https://fonts.bunny.net" />
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<script src="{{ asset("static/libs/ace/ace.js") }}"></script>
@include("layouts.partials.favicon") @include("layouts.partials.favicon")
<!-- Scripts --> <!-- Scripts -->

View File

@ -1,26 +1,34 @@
<nav <nav
class="fixed top-0 z-50 flex h-[64px] w-full items-center border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800" class="fixed top-0 z-50 flex h-[64px] w-full items-center border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
> >
<div class="w-full px-3 py-3 lg:px-5 lg:pl-3"> <div class="w-full">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center justify-start"> <div class="flex items-center justify-start">
<button <div
data-drawer-target="logo-sidebar" class="flex items-center justify-start border-r border-gray-200 px-3 py-3 dark:border-gray-700 md:w-64"
data-drawer-toggle="logo-sidebar"
aria-controls="logo-sidebar"
type="button"
class="inline-flex items-center rounded-md p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600 sm:hidden"
> >
<span class="sr-only">Open sidebar</span> <button
<x-heroicon name="o-bars-3-center-left" class="h-6 w-6" /> data-drawer-target="logo-sidebar"
</button> data-drawer-toggle="logo-sidebar"
<a href="/" class="ms-2 flex md:me-24"> aria-controls="logo-sidebar"
<div class="flex items-center justify-start text-3xl font-extrabold"> type="button"
<x-application-logo class="h-9 w-9 rounded-md" /> class="inline-flex items-center rounded-md p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600 sm:hidden"
<span class="ml-1 hidden sm:block">Deploy</span> >
</div> <span class="sr-only">Open sidebar</span>
</a> <x-heroicon name="o-bars-3-center-left" class="h-6 w-6" />
<div class="h-[64px] w-1 border-r border-gray-200 px-3 dark:border-gray-700 md:px-0"></div> </button>
<a href="/" class="ms-2 flex md:me-24">
<div class="relative flex items-center justify-start text-3xl font-extrabold">
<x-application-logo class="h-9 w-9 rounded-md" />
<span class="ml-1 hidden md:block">Deploy</span>
<span
class="absolute bottom-0 left-0 right-0 rounded-b-md bg-gray-700/60 text-center text-xs text-white md:relative md:ml-1 md:block md:bg-inherit md:text-inherit"
>
{{ vito_version() }}
</span>
</div>
</a>
</div>
<div class="ml-5 cursor-pointer" x-data=""> <div class="ml-5 cursor-pointer" x-data="">
<div <div
class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px-4 py-2 text-sm text-gray-900 focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:focus:ring-gray-600" class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px-4 py-2 text-sm text-gray-900 focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:focus:ring-gray-600"
@ -31,7 +39,7 @@ class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center"> <div class="flex items-center px-3 py-3">
<div class="mr-3"> <div class="mr-3">
@include("layouts.partials.color-scheme") @include("layouts.partials.color-scheme")
</div> </div>

View File

@ -1,5 +1,5 @@
<div data-tooltip="Project" class="cursor-pointer"> <div data-tooltip="Project" class="cursor-pointer">
<x-dropdown align="left"> <x-dropdown width="full">
<x-slot:trigger> <x-slot:trigger>
<div> <div>
<div <div

View File

@ -137,8 +137,8 @@
x-on:click="close" x-on:click="close"
class="fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] items-center bg-gray-500 opacity-75 dark:bg-gray-900" class="fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] items-center bg-gray-500 opacity-75 dark:bg-gray-900"
></div> ></div>
<div class="absolute z-[1000] mt-20 lg:scale-110"> <div class="absolute left-1 right-1 z-[1000] mt-20 md:left-auto md:right-auto lg:scale-110">
<div class="w-[500px]"> <div class="w-full px-10 md:w-[500px]">
<x-text-input <x-text-input
id="search-input" id="search-input"
x-ref="input" x-ref="input"

View File

@ -38,6 +38,17 @@ class="p-6"
@endif @endif
@endforeach @endforeach
</x-select-input> </x-select-input>
<x-input-help>
Check
<a
href="https://vitodeploy.com/settings/source-controls.html"
class="text-primary-500"
target="_blank"
>
here
</a>
to see what permissions you need
</x-input-help>
@error("provider") @error("provider")
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />
@enderror @enderror

View File

@ -3,6 +3,10 @@
@include("site-settings.partials.change-php-version") @include("site-settings.partials.change-php-version")
@if ($site->source_control_id)
@include("site-settings.partials.update-source-control")
@endif
@include("site-settings.partials.update-v-host") @include("site-settings.partials.update-v-host")
<x-card> <x-card>

View File

@ -0,0 +1,32 @@
<x-card>
<x-slot name="title">{{ __("Update Source Control") }}</x-slot>
<x-slot name="description">
{{ __("You can switch the source control profile (token) in case of token expiration. Keep in mind that it must be the same account and provider") }}
</x-slot>
<form
id="update-source-control"
hx-post="{{ route("servers.sites.settings.source-control", ["server" => $server, "site" => $site]) }}"
hx-swap="outerHTML"
hx-select="#update-source-control"
hx-ext="disable-element"
hx-disable-element="#btn-update-source-control"
class="space-y-6"
>
@include(
"sites.partials.create.fields.source-control",
[
"sourceControls" => \App\Models\SourceControl::query()
->where("provider", $site->sourceControl()?->provider)
->get(),
]
)
</form>
<x-slot name="actions">
<x-primary-button id="btn-update-source-control" form="update-source-control" hx-disable>
{{ __("Save") }}
</x-primary-button>
</x-slot>
</x-card>

View File

@ -22,9 +22,9 @@ class="space-y-6"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
<div id="vhost-container"> <div id="vhost-container">
<x-code-editor id="vhost" name="vhost" rows="10" class="mt-1 block w-full"> <x-textarea id="vhost" name="vhost" rows="10" class="mt-1 block min-h-[400px] w-full">
{{ session()->has("vhost") ? session()->get("vhost") : "Loading..." }} {{ session()->has("vhost") ? session()->get("vhost") : "Loading..." }}
</x-code-editor> </x-textarea>
</div> </div>
@error("vhost") @error("vhost")
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />

View File

@ -0,0 +1,15 @@
<x-site-layout :site="$site">
<x-slot name="pageTitle">{{ $site->domain }}</x-slot>
<x-live id="site">
@if ($site->status === \App\Enums\SiteStatus::INSTALLING)
@include("sites.partials.installing", ["site" => $site])
@endif
@if ($site->status === \App\Enums\SiteStatus::INSTALLATION_FAILED)
@include("sites.partials.installation-failed", ["site" => $site])
@endif
</x-live>
@include("server-logs.partials.logs-list", ["server" => $site->server, "site" => $site])
</x-site-layout>

View File

@ -6,7 +6,7 @@
@foreach ($sourceControls as $sourceControl) @foreach ($sourceControls as $sourceControl)
<option <option
value="{{ $sourceControl->id }}" value="{{ $sourceControl->id }}"
@if($sourceControl->id === old('source_control')) selected @endif @if($sourceControl->id == old('source_control', isset($site) ? $site->source_control_id : null)) selected @endif
> >
{{ $sourceControl->profile }} {{ $sourceControl->profile }}
({{ $sourceControl->provider }}) ({{ $sourceControl->provider }})

View File

@ -1,17 +0,0 @@
<div>
@if ($site->status === \App\Enums\SiteStatus::INSTALLING)
@include("sites.partials.installing", ["site" => $site])
@include("server-logs.partials.logs-list", ["server" => $site->server, "site" => $site])
@endif
@if ($site->status === \App\Enums\SiteStatus::INSTALLATION_FAILED)
@include("sites.partials.installation-failed", ["site" => $site])
@include("server-logs.partials.logs-list", ["server" => $site->server, "site" => $site])
@endif
@if ($site->status === \App\Enums\SiteStatus::READY)
@include("application." . $site->type . "-app", ["site" => $site])
@endif
</div>

View File

@ -1,5 +1,5 @@
<x-site-layout :site="$site"> <x-site-layout :site="$site">
<x-slot name="pageTitle">{{ $site->domain }}</x-slot> <x-slot name="pageTitle">{{ $site->domain }}</x-slot>
@include("sites.partials.show-site") @include("application." . $site->type . "-app", ["site" => $site])
</x-site-layout> </x-site-layout>

View File

@ -1,7 +1,9 @@
<?php <?php
// git hook // git hook
use App\Http\Controllers\GitHookController; use App\Http\Controllers\API\GitHookController;
use App\Http\Controllers\API\HealthController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::any('git-hooks', GitHookController::class)->name('git-hooks'); Route::get('health', HealthController::class)->name('api.health');
Route::any('git-hooks', GitHookController::class)->name('api.git-hooks');

View File

@ -36,6 +36,7 @@
Route::post('/create', [SiteController::class, 'store']); Route::post('/create', [SiteController::class, 'store']);
Route::get('/{site}', [SiteController::class, 'show'])->name('servers.sites.show'); Route::get('/{site}', [SiteController::class, 'show'])->name('servers.sites.show');
Route::delete('/{site}', [SiteController::class, 'destroy'])->name('servers.sites.destroy'); Route::delete('/{site}', [SiteController::class, 'destroy'])->name('servers.sites.destroy');
Route::get('/{site}/installing', [SiteController::class, 'installing'])->name('servers.sites.installing');
// site application // site application
Route::post('/{site}/application/deploy', [ApplicationController::class, 'deploy'])->name('servers.sites.application.deploy'); Route::post('/{site}/application/deploy', [ApplicationController::class, 'deploy'])->name('servers.sites.application.deploy');
@ -64,6 +65,7 @@
Route::get('/{site}/settings/vhost', [SiteSettingController::class, 'getVhost'])->name('servers.sites.settings.vhost'); Route::get('/{site}/settings/vhost', [SiteSettingController::class, 'getVhost'])->name('servers.sites.settings.vhost');
Route::post('/{site}/settings/vhost', [SiteSettingController::class, 'updateVhost']); Route::post('/{site}/settings/vhost', [SiteSettingController::class, 'updateVhost']);
Route::post('/{site}/settings/php', [SiteSettingController::class, 'updatePHPVersion'])->name('servers.sites.settings.php'); Route::post('/{site}/settings/php', [SiteSettingController::class, 'updatePHPVersion'])->name('servers.sites.settings.php');
Route::post('/{site}/settings/source-control', [SiteSettingController::class, 'updateSourceControl'])->name('servers.sites.settings.source-control');
// site logs // site logs
Route::get('/{site}/logs', [SiteLogController::class, 'index'])->name('servers.sites.logs'); Route::get('/{site}/logs', [SiteLogController::class, 'index'])->name('servers.sites.logs');

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
export VITO_VERSION="1.x"
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a export NEEDRESTART_MODE=a
@ -151,11 +152,13 @@ ln -s /etc/nginx/sites-available/vito /etc/nginx/sites-enabled/
service nginx restart service nginx restart
rm -rf /home/${V_USERNAME}/vito rm -rf /home/${V_USERNAME}/vito
git config --global core.fileMode false git config --global core.fileMode false
git clone -b 1.x ${V_REPO} /home/${V_USERNAME}/vito git clone -b ${VITO_VERSION} ${V_REPO} /home/${V_USERNAME}/vito
find /home/${V_USERNAME}/vito -type d -exec chmod 755 {} \; find /home/${V_USERNAME}/vito -type d -exec chmod 755 {} \;
find /home/${V_USERNAME}/vito -type f -exec chmod 644 {} \; find /home/${V_USERNAME}/vito -type f -exec chmod 644 {} \;
cd /home/${V_USERNAME}/vito && git config core.fileMode false cd /home/${V_USERNAME}/vito && git config core.fileMode false
cd /home/${V_USERNAME}/vito && composer install --no-dev cd /home/${V_USERNAME}/vito
git checkout $(git tag -l --merged ${VITO_VERSION} --sort=-v:refname | head -n 1)
composer install --no-dev
cp .env.prod .env cp .env.prod .env
touch /home/${V_USERNAME}/vito/storage/database.sqlite touch /home/${V_USERNAME}/vito/storage/database.sqlite
php artisan key:generate php artisan key:generate

View File

@ -4,7 +4,9 @@ cd /home/vito/vito
php artisan down php artisan down
git pull git fetch --all
git checkout $(git tag -l --merged 1.x --sort=-v:refname | head -n 1)
composer install --no-dev composer install --no-dev

View File

@ -212,7 +212,7 @@ public function test_git_hook_deployment(): void
'content' => 'git pull', 'content' => 'git pull',
]); ]);
$this->post(route('git-hooks'), [ $this->post(route('api.git-hooks'), [
'secret' => 'secret', 'secret' => 'secret',
])->assertSessionDoesntHaveErrors(); ])->assertSessionDoesntHaveErrors();
@ -240,7 +240,7 @@ public function test_git_hook_deployment_invalid_secret(): void
'content' => 'git pull', 'content' => 'git pull',
]); ]);
$this->post(route('git-hooks'), [ $this->post(route('api.git-hooks'), [
'secret' => 'invalid-secret', 'secret' => 'invalid-secret',
])->assertNotFound(); ])->assertNotFound();

View File

@ -46,6 +46,48 @@ public function test_create_site(array $inputs): void
]); ]);
} }
/**
* @dataProvider create_failure_data
*/
public function test_create_site_failed_due_to_source_control(int $status): void
{
$inputs = [
'type' => SiteType::LARAVEL,
'domain' => 'example.com',
'alias' => 'www.example.com',
'php_version' => '8.2',
'web_directory' => 'public',
'repository' => 'test/test',
'branch' => 'main',
'composer' => true,
];
SSH::fake();
Http::fake([
'https://api.github.com/repos/*' => Http::response([
], $status),
]);
$this->actingAs($this->user);
/** @var \App\Models\SourceControl $sourceControl */
$sourceControl = \App\Models\SourceControl::factory()->create([
'provider' => SourceControl::GITHUB,
]);
$inputs['source_control'] = $sourceControl->id;
$this->post(route('servers.sites.create', [
'server' => $this->server,
]), $inputs)->assertSessionHasErrors();
$this->assertDatabaseMissing('sites', [
'domain' => 'example.com',
'status' => SiteStatus::READY,
]);
}
public function test_see_sites_list(): void public function test_see_sites_list(): void
{ {
$this->actingAs($this->user); $this->actingAs($this->user);
@ -103,6 +145,58 @@ public function test_change_php_version(): void
$this->assertEquals('8.2', $site->php_version); $this->assertEquals('8.2', $site->php_version);
} }
public function test_update_source_control(): void
{
SSH::fake();
$this->actingAs($this->user);
Http::fake([
'https://api.github.com/repos/*' => Http::response([
], 201),
]);
/** @var \App\Models\SourceControl $sourceControl */
$sourceControl = \App\Models\SourceControl::factory()->create([
'provider' => SourceControl::GITHUB,
]);
$this->post(route('servers.sites.settings.source-control', [
'server' => $this->server,
'site' => $this->site,
]), [
'source_control' => $sourceControl->id,
])->assertSessionDoesntHaveErrors();
$this->site->refresh();
$this->assertEquals($sourceControl->id, $this->site->source_control_id);
}
public function test_failed_to_update_source_control(): void
{
SSH::fake();
$this->actingAs($this->user);
Http::fake([
'https://api.github.com/repos/*' => Http::response([
], 404),
]);
/** @var \App\Models\SourceControl $sourceControl */
$sourceControl = \App\Models\SourceControl::factory()->create([
'provider' => SourceControl::GITHUB,
]);
$this->post(route('servers.sites.settings.source-control', [
'server' => $this->server,
'site' => $this->site,
]), [
'source_control' => $sourceControl->id,
])->assertSessionHasErrors();
}
public function test_update_v_host(): void public function test_update_v_host(): void
{ {
SSH::fake(); SSH::fake();
@ -170,6 +264,15 @@ public static function create_data(): array
]; ];
} }
public static function create_failure_data(): array
{
return [
[401],
[403],
[404],
];
}
public function test_see_logs(): void public function test_see_logs(): void
{ {
$this->actingAs($this->user); $this->actingAs($this->user);