Compare commits

...

12 Commits
1.1.1 ... 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
a0af4e3e9d Merge pull request #128 from vitodeploy/1.x
Merge
2024-03-24 10:07:20 +01:00
49 changed files with 378 additions and 178 deletions

View File

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

View File

@ -3,6 +3,8 @@
namespace App\Actions\Site;
use App\Enums\SiteStatus;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Models\Server;
@ -19,7 +21,6 @@
class CreateSite
{
/**
* @throws SourceControlIsNotConnected
* @throws ValidationException
*/
public function create(Server $server, array $input): Site
@ -47,7 +48,15 @@ public function create(Server $server, array $input): Site
}
} catch (SourceControlIsNotConnected) {
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;
use Exception;
class SSHAuthenticationError extends Exception
class SSHAuthenticationError extends SSHError
{
//
}

View File

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

View File

@ -133,7 +133,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
$this->log?->write($output);
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;

View File

@ -1,10 +1,11 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\API;
use App\Actions\Site\Deploy;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Http\Controllers\Controller;
use App\Models\GitHook;
use App\Models\ServerLog;
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\UpdateEnv;
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
@ -24,12 +26,14 @@ public function deploy(Server $server, Site $site): HtmxResponse
app(Deploy::class)->run($site);
Toast::success('Deployment started!');
} catch (SourceControlIsNotConnected $e) {
Toast::error($e->getMessage());
return htmx()->redirect(route('source-controls'));
} catch (SourceControlIsNotConnected) {
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!');
}
return htmx()->back();
@ -83,6 +87,12 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
Toast::success('Auto deployment has been enabled.');
} catch (SourceControlIsNotConnected) {
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\DeleteSite;
use App\Enums\SiteStatus;
use App\Enums\SiteType;
use App\Facades\Toast;
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', [
'server' => $server,
'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
{
app(DeleteSite::class)->delete($site);

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Actions\Site\UpdateSourceControl;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
@ -63,4 +64,13 @@ public function updatePHPVersion(Server $server, Site $site, Request $request):
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
*/
protected $proxies;
protected $proxies = '*';
/**
* The headers that should be used to detect proxies.
@ -20,7 +20,7 @@ class TrustProxies extends Middleware
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |

View File

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

View File

@ -29,3 +29,8 @@ function htmx(): 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
# app
COPY . /var/www/html
RUN rm -rf /var/www/html/vendor
RUN rm -rf /var/www/html/.env
RUN rm -rf /var/www/html
RUN git clone -b 1.x https://github.com/vitodeploy/vito.git /var/www/html
RUN git checkout $(git tag -l --merged 1.x --sort=-v:refname | head -n 1)
RUN composer install --no-dev --prefer-dist
RUN chown -R www-data:www-data /var/www/html \
&& 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",
"tippy.js": "^6.3.7",
"toastr": "^2.1.4",
"vite": "^4.5.2"
"vite": "^4.5.3"
}
},
"node_modules/@esbuild/android-arm": {
@ -1812,9 +1812,9 @@
"dev": true
},
"node_modules/vite": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",
@ -3011,9 +3011,9 @@
"dev": true
},
"vite": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"requires": {
"esbuild": "^0.18.10",

View File

@ -22,6 +22,6 @@
"tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"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": {
"file": "assets/app-2c6e7578.css",
"file": "assets/app-e3775b0a.css",
"isEntry": true,
"src": "resources/css/app.css"
},
@ -12,7 +12,7 @@
"css": [
"assets/app-a1ae07b3.css"
],
"file": "assets/app-5f99a92f.js",
"file": "assets/app-4c0bf3b2.js",
"isEntry": true,
"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,12 +23,10 @@ window.htmx.defineExtension('disable-element', {
});
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRF-TOKEN'] = document.head.querySelector('meta[name="csrf-token"]').content;
if (window.getSelection) { window.getSelection().removeAllRanges(); }
else if (document.selection) { document.selection.empty(); }
// if (window.getSelection) { window.getSelection().removeAllRanges(); }
// else if (document.selection) { document.selection.empty(); }
});
let activeElement = null;
document.body.addEventListener('htmx:beforeRequest', (event) => {
activeElement = document.activeElement;
let targetElements = event.target.querySelectorAll('[hx-disable]');
for (let i = 0; i < targetElements.length; i++) {
targetElements[i].disabled = true;
@ -46,11 +44,6 @@ document.body.addEventListener('htmx:afterSwap', (event) => {
return reference.getAttribute('data-tooltip');
},
});
if (activeElement) {
activeElement.blur();
activeElement.focus();
activeElement = null;
}
});
import toastr from 'toastr';

View File

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

View File

@ -12,11 +12,13 @@ class="p-6"
{{ __("Deployment Script") }}
</h2>
<div class="mt-6">A bash script that will be executed when you run the deployment process.</div>
<div class="mt-6">
<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) }}
</x-code-editor>
</x-textarea>
@error("script")
<x-input-error class="mt-2" :messages="$message" />
@enderror

View File

@ -21,9 +21,9 @@ class="mt-6"
>
<x-input-label for="env" :value="__('.env')" />
<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...") }}
</x-code-editor>
</x-textarea>
</div>
@error("env")
<x-input-error class="mt-2" :messages="$message" />

View File

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

View File

@ -1,41 +0,0 @@
@props([
"id",
"name",
"disabled" => false,
"lang" => "text",
])
<div
x-data="{
editorId: @js($id),
disabled: @js($disabled),
lang: @js($lang),
init() {
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]) }}',
async run() {
this.running = true
this.output = 'Running...\n'
this.output = this.command + '\n'
const fetchOptions = {
method: 'POST',
headers: {
@ -23,6 +23,7 @@
}),
}
this.command = ''
const response = await fetch(this.runUrl, fetchOptions)
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')

View File

@ -21,8 +21,6 @@
<link rel="preconnect" href="https://fonts.bunny.net" />
<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")
<!-- Scripts -->

View File

@ -1,26 +1,34 @@
<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"
>
<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-start">
<button
data-drawer-target="logo-sidebar"
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"
<div
class="flex items-center justify-start border-r border-gray-200 px-3 py-3 dark:border-gray-700 md:w-64"
>
<span class="sr-only">Open sidebar</span>
<x-heroicon name="o-bars-3-center-left" class="h-6 w-6" />
</button>
<a href="/" class="ms-2 flex md:me-24">
<div class="flex items-center justify-start text-3xl font-extrabold">
<x-application-logo class="h-9 w-9 rounded-md" />
<span class="ml-1 hidden sm:block">Deploy</span>
</div>
</a>
<div class="h-[64px] w-1 border-r border-gray-200 px-3 dark:border-gray-700 md:px-0"></div>
<button
data-drawer-target="logo-sidebar"
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>
<x-heroicon name="o-bars-3-center-left" class="h-6 w-6" />
</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="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 class="flex items-center">
<div class="flex items-center px-3 py-3">
<div class="mr-3">
@include("layouts.partials.color-scheme")
</div>

View File

@ -137,8 +137,8 @@
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"
></div>
<div class="absolute z-[1000] mt-20 lg:scale-110">
<div class="w-[500px]">
<div class="absolute left-1 right-1 z-[1000] mt-20 md:left-auto md:right-auto lg:scale-110">
<div class="w-full px-10 md:w-[500px]">
<x-text-input
id="search-input"
x-ref="input"

View File

@ -38,6 +38,17 @@ class="p-6"
@endif
@endforeach
</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")
<x-input-error class="mt-2" :messages="$message" />
@enderror

View File

@ -3,6 +3,10 @@
@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")
<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"
>
<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..." }}
</x-code-editor>
</x-textarea>
</div>
@error("vhost")
<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)
<option
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->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-slot name="pageTitle">{{ $site->domain }}</x-slot>
@include("sites.partials.show-site")
@include("application." . $site->type . "-app", ["site" => $site])
</x-site-layout>

View File

@ -1,7 +1,9 @@
<?php
// git hook
use App\Http\Controllers\GitHookController;
use App\Http\Controllers\API\GitHookController;
use App\Http\Controllers\API\HealthController;
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::get('/{site}', [SiteController::class, 'show'])->name('servers.sites.show');
Route::delete('/{site}', [SiteController::class, 'destroy'])->name('servers.sites.destroy');
Route::get('/{site}/installing', [SiteController::class, 'installing'])->name('servers.sites.installing');
// site application
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::post('/{site}/settings/vhost', [SiteSettingController::class, 'updateVhost']);
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
Route::get('/{site}/logs', [SiteLogController::class, 'index'])->name('servers.sites.logs');

View File

@ -1,5 +1,6 @@
#!/bin/bash
export VITO_VERSION="1.x"
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a
@ -151,11 +152,13 @@ ln -s /etc/nginx/sites-available/vito /etc/nginx/sites-enabled/
service nginx restart
rm -rf /home/${V_USERNAME}/vito
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 f -exec chmod 644 {} \;
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
touch /home/${V_USERNAME}/vito/storage/database.sqlite
php artisan key:generate

View File

@ -4,7 +4,9 @@ cd /home/vito/vito
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

View File

@ -212,7 +212,7 @@ public function test_git_hook_deployment(): void
'content' => 'git pull',
]);
$this->post(route('git-hooks'), [
$this->post(route('api.git-hooks'), [
'secret' => 'secret',
])->assertSessionDoesntHaveErrors();
@ -240,7 +240,7 @@ public function test_git_hook_deployment_invalid_secret(): void
'content' => 'git pull',
]);
$this->post(route('git-hooks'), [
$this->post(route('api.git-hooks'), [
'secret' => 'invalid-secret',
])->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
{
$this->actingAs($this->user);
@ -103,6 +145,58 @@ public function test_change_php_version(): void
$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
{
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
{
$this->actingAs($this->user);