Added built in Config Editor

LittleLink Custom now includes an .env config editor. This editor can be accessed via the Admin Panel under Admin>Config.
This editor allows admins to edit, backup, download and upload the .env configuration file. All in all, the new feature, allows users to more easily edit the configuration file, contributing to my goal of making LittleLink Custom easier to use.

Read more about this topic on the Blog https://blog.littlelink-custom.com/built-in-config-editor
This commit is contained in:
Julian Prieber 2022-03-16 14:16:03 +01:00
parent 9bdf937e4b
commit fef2e09aa9
14 changed files with 918 additions and 38 deletions

71
.env
View File

@ -1,44 +1,45 @@
APP_NAME="LittleLink Custom"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=
#App Settings=Changes settings regarding your LittleLink Custom installation. You probably only want to change the App Name setting.
#=App_Name changes the displayed name for the App in the title, for example.
App_Name="LittleLink Custom"
App_Key=base64:khLI7djHyA97qfrA+rfz1YUFukELiN6Bk9gQ19+9zwk=
App_URL=
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=sqlite
#Debug Settings=Changes if your page should display a full error description instead of a generic error 500
#=App_debug either true or false. You might want to change this to false after you're done installing, but it's very useful for troubleshooting.
App_debug=true
#=App_env either local or production. Change this to production if you set the value above to false
App_env=local
Log_channel=stack
Log_level=debug
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
#Database Settings=Should be left alone. If you wish to use mysql you'd have to seed the database again.
DB_connection=sqlite
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
#Mail Settings=LittleLink Custom comes with a free to use built-in SMTP server for sending mail. You can leave this setting as is, if you wish to use this service please read our terms and conditions at llc-mail.tru.io. If you do not wish to use the built-in SMTP server, change the setting below
#=Mail_mailer either smtp or built-in. Make sure to change this setting if you want to add a custom SMTP server.
Mail_mailer=built-in
Mail_host=
Mail_port=
Mail_username=
Mail_password=
Mail_encryption=
Mail_from_address=
Mail_from_name="${app_name}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
#Cache Settings=Completely optional
Memcached_host=127.0.0.1
Redis_host=127.0.0.1
Redis_password=null
Redis_port=6379
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
#Miscellaneous Settings=Should be left alone if you don't know what you're doing.
Broadcast_driver=log
Cache_driver=file
Queue_connection=sync
Session_driver=file
Session_lifetime=120

View File

@ -8,6 +8,7 @@
"php": "^7.3|^8.0",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"geo-sot/laravel-env-editor": "^1.1",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^8.12",
"laravel/tinker": "^2.5"
@ -58,4 +59,3 @@
"minimum-stability": "dev",
"prefer-stable": true
}

66
composer.lock generated
View File

@ -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": "0ad35353a22e3d26fcc6897fcc5dc893",
"content-hash": "90a7893b152b65a22c6ed40595cd35c0",
"packages": [
{
"name": "asm89/stack-cors",
@ -557,6 +557,68 @@
],
"time": "2021-04-26T11:24:25+00:00"
},
{
"name": "geo-sot/laravel-env-editor",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/GeoSot/Laravel-EnvEditor.git",
"reference": "d519594fcbc5dd9d35d47d56a96aae17f12c685f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GeoSot/Laravel-EnvEditor/zipball/d519594fcbc5dd9d35d47d56a96aae17f12c685f",
"reference": "d519594fcbc5dd9d35d47d56a96aae17f12c685f",
"shasum": ""
},
"require": {
"laravel/framework": ">=8",
"php": ">=7.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.4",
"nunomaduro/larastan": "^1.0",
"orchestra/testbench": "^6"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"GeoSot\\EnvEditor\\ServiceProvider"
],
"aliases": {
"EnvEditor": "GeoSot\\EnvEditor\\Facades\\EnvEditor"
}
}
},
"autoload": {
"psr-4": {
"GeoSot\\EnvEditor\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Geo Sot",
"email": "geo.sotis@gmail.com"
}
],
"description": "A laravel Package that supports .Env File, editing and backup ",
"keywords": [
"EnvEditor",
"geo-sot",
"laravel",
"laravel-env-editor"
],
"support": {
"issues": "https://github.com/GeoSot/Laravel-EnvEditor/issues",
"source": "https://github.com/GeoSot/Laravel-EnvEditor/tree/v1.1.0"
},
"time": "2022-01-25T17:13:30+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.0.1",
@ -7457,5 +7519,5 @@
"php": "^7.3|^8.0"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.2.0"
}

46
config/env-editor.php Normal file
View File

@ -0,0 +1,46 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Files Config
|--------------------------------------------------------------------------
*/
'paths' => [
// .env file directory
'env' => base_path(),
//backup files directory
'backupDirectory' => 'backups',
],
// .env file name
'envFileName' => '.env',
/*
|--------------------------------------------------------------------------
| Routes group config
|--------------------------------------------------------------------------
|
*/
'route' => [
// Prefix url for route Group
'prefix' => 'env-editor',
// Routes base name
'name' => 'env-editor',
// Middleware(s) applied on route Group
'middleware' => ['web', 'admin'],
],
/* ------------------------------------------------------------------------------------------------
| Time Format for Views and parsed backups
| ------------------------------------------------------------------------------------------------
*/
'timeFormat' => 'd/m/Y H:i:s',
/* ------------------------------------------------------------------------------------------------
| Set Views options
| ------------------------------------------------------------------------------------------------
| Here you can set The "extends" blade of index.blade.php
*/
'layout' => 'env-editor::layout',
];

View File

@ -0,0 +1,89 @@
<?php
return [
'menuTitle' => '.env Editor',
'controllerMessages' => [
'backupWasCreated' => 'A new backup was created',
'fileWasRestored' => 'The backup file ":name", was restored as default .env',
'fileWasDeleted' => 'The backup file ":name", was deleted',
'currentEnvWasReplacedByTheUploadedFile' => 'File was uploaded and become the new .env file',
'uploadedFileSavedAsBackup' => 'File was uploaded as backup with the name ":name"',
'keyWasAdded' => 'Key ":name" was added',
'keyWasEdited' => 'Key ":name" has ben updated',
'keyWasDeleted' => 'Key ":name" was Deleted',
],
'views' => [
'tabTitles' => [
'upload' => 'Upload',
'backup' => 'Backups',
'currentEnv' => 'Current .env',
],
'currentEnv' => [
'title' => 'Current .env file Content',
'tableTitles' => [
'key' => 'Key',
'value' => 'Value',
'actions' => 'Actions',
],
'btn' => [
'edit' => 'Edit File',
'delete' => 'Delete Key',
'addAfterKey' => 'Add new key after this key',
'addNewKey' => 'Add New key',
'deleteConfigCache' => 'Clear config cache',
'deleteConfigCacheDesc' => 'On production environments changed values may not applied immediately cause of cached config. So you may try to un-cache it',
],
'modal' => [
'title' => [
'new' => 'New Key',
'edit' => 'Edit Key',
'delete' => 'Delete Key',
],
'input' => [
'key' => 'Key',
'value' => 'Value',
],
'btn' => [
'close' => 'Close',
'new' => 'Add Key',
'edit' => 'Update Key',
'delete' => 'Delete Key',
],
],
],
'upload' => [
'title' => 'Here You can upload a new ".env" file as a backup or to replace the current ".env"',
'selectFilePrompt' => 'Select File',
'btn' => [
'clearFile' => 'Cancel',
'uploadAsBackup' => 'Upload as backup',
'uploadAndReplace' => 'Upload and replace current .env',
],
],
'backup' => [
'title' => 'Here you can see a list of saved backup files (if you have), you can create a new one, or download the .env file',
'tableTitles' => [
'filename' => 'File Name',
'created_at' => 'Creation Date',
'actions' => 'Actions',
],
'noBackUpItems' => 'There are no backups on your chosen directory. <br> You can make your first backup by pressing the "Get a new BackUp" button',
'btn' => [
'backUpCurrentEnv' => 'Get a new BackUp',
'downloadCurrentEnv' => 'Download current .env',
'download' => 'Download File',
'delete' => 'Delete File',
'restore' => 'Restore File',
'viewContent' => 'View file Contents',
],
],
],
'exceptions' => [
'fileNotExists' => 'File ":name" does not Exists !!!',
'keyAlreadyExists' => 'Key ":name" already Exists !!!',
'keyNotExists' => 'Key ":name" does not Exists !!!',
'provideFileName' => 'You have to provide a FileName !!!',
],
];

View File

@ -74,6 +74,9 @@
<li class="active">
<a href="#adminSubmenu" data-toggle="collapse" aria-expanded="false" class="dropdown-toggle">Admin</a>
<ul class="collapse list-unstyled" id="adminSubmenu">
<li>
<a href="{{ url('env-editor') }}">Config</a>
</li>
<li>
<a href="{{ url('panel/users/all') }}">Users</a>
</li>

View File

@ -0,0 +1,118 @@
@php($translatePrefix='env-editor::env-editor.views.backup.')
<template id="env-editor-backups">
<div>
<div class="h5 my-4">{{__($translatePrefix.'title')}}</div>
<div>
<button class="btn-info btn " @click="createBackUp">{{__($translatePrefix.'btn.backUpCurrentEnv')}}</button>
<a class="btn-info btn" href="{{route(config($package.'.route.name').'.download')}}">{{__($translatePrefix.'btn.downloadCurrentEnv')}}</a>
</div>
<div class=" my-3">
<div v-if="items.length" class="table-responsive">
<table id="env-editor-table-accordion" class="table">
<thead>
<tr class="table-secondary">
<th scope="col">{{__($translatePrefix.'tableTitles.filename')}}</th>
<th scope="col">{{__($translatePrefix.'tableTitles.created_at')}}</th>
<th scope="col">{{__($translatePrefix.'tableTitles.actions')}}</th>
</tr>
</thead>
<tbody>
<template v-for="(item, index) in items">
<tr :key="item.real_name" :bind="item">
<th scope="row" class="font-weight-bold ">@{{ item.name }}</th>
<td>@{{ item.created_at_formatted }}</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-info" data-toggle="collapse" aria-expanded="false"
:data-target="'#collapse_'+item.real_name"
:aria-controls="'#collaps_'+item.real_name" title="{{__($translatePrefix.'btn.viewContent')}}"><span class="fas fa-eye"></span></button>
<a class="btn btn-info" :href="getDownLoadLink(item)" title="{{__($translatePrefix.'btn.download')}}"><span
class="fas fa-download"></span></a>
<button class="btn btn-secondary" @click="restore(item)" title="{{__($translatePrefix.'btn.restore')}}"><span class="fas fa-redo"></span>
</button>
<button class="btn btn-danger" @click="destroy(item)" title="{{__($translatePrefix.'btn.delete')}}"><span class="fas fa-trash"></span></button>
</div>
</td>
</tr>
<tr>
<td colspan="100%" class="p-0">
<div class="collapse" :id="'collapse_'+item.real_name" data-parent="#env-editor-table-accordion">
<div class="table-responsive table-sm px-3 pb-3">
<table class="w-100 bg-light">
<tr v-for="(dt, index) in item.parsed_data">
<td class="pl-3"><code>@{{ dt.key||'&nbsp;' }}</code></td>
<td><code>@{{ dt.value }}</code></td>
</tr>
</table>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="text-primary font-italic" v-else>{!! __($translatePrefix.'noBackUpItems') !!}</div>
</div>
</div>
</template>
@push('scripts')
<script>
const backUps = {
template: '#env-editor-backups',
data: () => {
return {
modalItem: '',
items: []
}
},
computed: {},
mounted() {
this.getItemsWithAjax();
envEventBus.$on('env:backupsChanged', () => {
this.getItemsWithAjax();
});
},
methods: {
getDownLoadLink(item) {
let downloadUrl = '{{route(config($package.'.route.name').'.download')}}/';
return downloadUrl + item.real_name;
},
createBackUp() {
let url = '{{route(config($package.'.route.name').'.createBackup')}}';
this.sendBasicAjaxRequest('post', url, 'backupsChanged');
},
restore(item) {
let url = '{{route(config($package.'.route.name').'.restoreBackup')}}/';
this.sendBasicAjaxRequest('post', url + item.real_name, 'changed');
},
destroy(item) {
let url = '{{route(config($package.'.route.name').'.destroyBackup')}}/';
this.sendBasicAjaxRequest('delete', url + item.real_name, 'backupsChanged');
},
sendBasicAjaxRequest($method, $url, $eventToTrigger) {
envClient($url, { method: $method }).then((data) => {
if (data.message) {
envAlert('info', data.message);
}
envEventBus.$emit('env:' + $eventToTrigger);
})
},
getItemsWithAjax() {
envClient('{{route(config($package.'.route.name').'.getBackups')}}')
.then(data => this.items = Object.values(data.items))
}
},
};
</script>
@endpush

View File

@ -0,0 +1,33 @@
@php($translatePrefix='env-editor::env-editor.views.currentEnv.')
<template id="env-editor-config-actions">
<div>
<button class="btn-outline-dark btn btn-sm" title="{{__($translatePrefix.'btn.deleteConfigCacheDesc')}}"
@click="deleteConfigCache">{{__($translatePrefix.'btn.deleteConfigCache')}}</button>
</div>
</template>
@push('scripts')
<script>
let configActions = {
template: '#env-editor-config-actions',
methods: {
deleteConfigCache() {
this.submit('delete', '{{route(config($package.'.route.name').'.clearConfigCache')}}');
},
submit(method, url) {
envClient(url,{
method: method
}).then(data => {
if (data.message) {
envAlert('info', data.message);
}
})
},
}
};
</script>
@endpush

View File

@ -0,0 +1,87 @@
@php($translatePrefix='env-editor::env-editor.views.currentEnv.')
<template id="env-editor-main-tab">
<div>
<div class="h5 my-4">{{__($translatePrefix.'title')}}</div>
<div class="py-3 text-right">
<button class="btn btn-info" @click="addNew()">{{__($translatePrefix.'btn.addNewKey')}}</button>
</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr class="table-secondary ">
<th class="py-2" scope="col">{{__($translatePrefix.'tableTitles.key')}}</th>
<th class="py-2" scope="col">{{__($translatePrefix.'tableTitles.value')}}</th>
<th class="py-2" scope="col">{{__($translatePrefix.'tableTitles.actions')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in items" :key="item.key" v-bind="item" v-if="!item.separator">
<th scope="row" class="font-weight-bold ">@{{ item.key }}</th>
<td>@{{ item.value }}</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-info" @click="edit(item)" title="{{__($translatePrefix.'btn.edit')}}"><span class="fas fa-edit"></span></button>
<button class="btn btn-secondary" @click="addAfter(item)" title="{{__($translatePrefix.'btn.addAfterKey')}}"><span class="fas fa-share"></span></button>
<button class="btn btn-danger" @click="remove(item)" title="{{__($translatePrefix.'btn.delete')}}"><span class="fas fa-trash"></span></button>
</div>
</td>
</tr>
<tr v-else>
<td colspan="100%">&nbsp;</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
@push('scripts')
<script>
const itemsWrapper = {
template: '#env-editor-main-tab',
data: () => {
return {
items: [],
}
},
mounted() {
envEventBus.$on('env:changed', () => {
this.getItemsWithAjax();
});
this.getItemsWithAjax()
},
methods: {
edit: function (item) {
envEventBus.$emit('env:item:edit', item);
},
addNew() {
envEventBus.$emit('env:item:new');
},
addAfter(item) {
let oldItem = {
key: null,
value: null,
group: item.group,
index: item.index + 0.1,
}
envEventBus.$emit('env:item:new', oldItem);
},
remove(item) {
envEventBus.$emit('env:item:delete', item);
},
getItemsWithAjax() {
envClient('{{route(config($package.'.route.name').'.index')}}')
.then(data => this.items = data.items)
}
},
}
;
</script>
@endpush

View File

@ -0,0 +1,152 @@
@php($translatePrefix='env-editor::env-editor.views.currentEnv.')
<template id="env-editor-modal">
<div id="env-editor-keys-modal" class=" modal fade " tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" :class="'text-'+actionClass">@{{ title }}</h5>
<button type="button" class="close" @click="hideModal()" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<input type="hidden" name="group" v-model.trim="modalItem.group">
<div class="form-group">
<label for="env_key" class="col-form-label">{{__($translatePrefix.'modal.input.key')}}:</label>
<input type="text" class="form-control" id="env_key" :readonly="readonly.key" v-model.trim="modalItem.key"/>
</div>
<div class="form-group">
<label for="env_value" class="col-form-label">{{__($translatePrefix.'modal.input.value')}}:</label>
<input type="text" class="form-control" id="env_value" :readonly="readonly.value" v-model.trim="modalItem.value"/>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" @click="hideModal()">{{__($translatePrefix.'modal.btn.close')}}</button>
<button id="save_evnVariable" type="button" class="btn " :class="'btn-'+actionClass" @click="submit()"> @{{ submitBtn }}</button>
</div>
</div>
</div>
</div>
</template>
@push('scripts')
<script>
let itemsModal = {
template: '#env-editor-modal',
data: () => {
return {
modal: '#env-editor-keys-modal',
type: '',
modalItem: {},
readonly: {
key: false,
value: false,
}
}
},
mounted() {
this.modalItem = this.newModalItem();
envEventBus.$on('env:item:edit', (item) => {
this.makeReadOnly('key');
this.show('edit', item);
}).$on('env:item:delete', (item) => {
this.makeReadOnly('key');
this.makeReadOnly('value');
this.show('delete', item);
}).$on('env:item:new', (item) => {
this.show('new', item);
});
},
computed: {
submitBtn() {
let values = {
'new': '{{__($translatePrefix."modal.btn.new")}}',
'edit': '{{__($translatePrefix."modal.btn.edit")}}',
'delete': '{{__($translatePrefix."modal.btn.delete")}}',
};
return values[this.type.toLowerCase()];
},
title() {
let values = {
'new': '{{__($translatePrefix."modal.title.new")}}',
'edit': '{{__($translatePrefix."modal.title.edit")}}',
'delete': '{{__($translatePrefix."modal.title.delete")}}',
};
return values[this.type.toLowerCase()] + ': ' + this.modalItem.key;
},
actionClass() {
switch (this.type) {
case 'delete':
return 'danger';
case 'edit':
return 'info';
default:
return 'success';
}
}
},
methods: {
newModalItem: () => {
return {
key: '',
value:'',
group: null,
}
},
hideModal() {
this.modalItem = this.newModalItem();
this.readonly.key = false;
this.readonly.value = false;
$(this.modal).modal('hide');
},
getAjaxMethod() {
switch (this.type) {
case 'delete':
return 'delete';
case 'edit':
return 'patch';
default:
return 'post'
}
},
makeReadOnly(arg = null) {
let $vm = this;
if (arg) {
$vm.readonly[arg] = true;
return;
}
Object.keys($vm.readonly).forEach(function (el) {
$vm.readonly[el] = true;
});
},
show(type = '', item) {
(item) ? this.modalItem = item : '';
this.type = type;
$(this.modal).modal('show')
},
submit() {
envClient('{{route(config($package.'.route.name').'.key')}}',{
method: this.getAjaxMethod(),
data: this.modalItem
}).then(data => {
if (data.message) {
envAlert('info', data.message);
}
envEventBus.$emit('env:changed')
}).then(() => {
this.hideModal();
});
},
}
};
</script>
@endpush

View File

@ -0,0 +1,92 @@
@php($translatePrefix='env-editor::env-editor.views.upload.')
<template id="env-editor-uploadFile">
<div>
<div class="h5 my-4">{{__($translatePrefix.'title')}}</div>
<div id="uploadEnvForm">
<div class="input-group mb-4">
<div class="custom-file ">
<input type="file" class="custom-file-input" :class="formInputNotValidClass" @change="fileInputChanged" lang="en">
<label class="custom-file-label" for="customFileLang">@{{ promptMsg }}</label>
</div>
<div class="invalid-feedback d-block" v-if="formIsInvalid">
@{{ errors }}
</div>
<div class="input-group-append" v-if="hasFile">
<button class="btn btn-outline-secondary" @click="clearFileInput" type="button">{{__($translatePrefix.'btn.clearFile')}}</button>
</div>
</div>
<div class="my-4">
<button class="btn-info btn " :disabled="!hasFile" @click="uploadAsBackUp">{{__($translatePrefix.'btn.uploadAsBackup')}}</button>
<button class="btn-warning btn " :disabled="!hasFile" @click="uploadAndReplaceCurrent">{{__($translatePrefix.'btn.uploadAndReplace')}}</button>
</div>
</div>
</div>
</template>
@push('scripts')
<script>
const fileUpload = {
template: '#env-editor-uploadFile',
data: () => {
return {
file: null,
fileName: null,
formIsInvalid: false,
errors: ''
}
},
computed: {
promptMsg() {
return this.hasFile ? this.fileName : "{{__($translatePrefix.'selectFilePrompt')}}"
},
hasFile() {
return (this.file !== null)
},
formInputNotValidClass() {
return this.formIsInvalid ? 'is-invalid ' : ''
},
},
methods: {
clearFileInput: function () {
this.file = null;
this.fileName = null;
},
uploadAsBackUp: function (e) {
return this.submitForm(e);
},
uploadAndReplaceCurrent: function (e) {
return this.submitForm(e, true);
},
fileInputChanged({ type, target }) {
this.fileName = target.files[0].name;
this.file = target.files[0]
},
submitForm(event, replaceCurrentEnv = false) {
this.formIsInvalid = false;
const formData = new FormData();
formData.append('file', this.file);
formData.append('replace_current', replaceCurrentEnv.toString());
envClient('{{route(config($package.'.route.name').'.upload')}}', {
body: formData,
method: 'post',
}).then(data => {
if (data.message) {
envAlert('info', data.message);
}
(replaceCurrentEnv) ? envEventBus.$emit('env:changed') : envEventBus.$emit('env:backupsChanged');
this.fileName = null;
this.file = null;
}).catch((error) => {
this.errors = error.errors.file[0];
this.formIsInvalid = true;
});
}
},
};
</script>
@endpush

View File

@ -0,0 +1,114 @@
@php($package='env-editor')
@php($translatePrefix='env-editor::env-editor.')
@extends(config("$package.layout"))
@push('documentTitle')
<i class="fas fa-cog" aria-hidden="true"></i>
{{trans('env-editor::env-editor.menuTitle')}}
@endpush
@section('content')
<div id="env-editor">
<div id="env-alerts"></div>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#current-env" role="tab">{{__($translatePrefix.'views.tabTitles.currentEnv')}}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#backup-env" role="tab">{{__($translatePrefix.'views.tabTitles.backup')}}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#upload-env" role="tab">{{__($translatePrefix.'views.tabTitles.upload')}}</a>
</li>
<li class="nav-item ml-auto">
<env-editor-config-actions></env-editor-config-actions>
</li>
</ul>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active p-3" id="current-env" role="tabpanel" aria-labelledby="nav-home-tab">
<env-main-tab></env-main-tab>
</div>
<div class="tab-pane fade p-3" id="backup-env" role="tabpanel" aria-labelledby="nav-profile-tab">
<env-editor-backups></env-editor-backups>
</div>
<div class="tab-pane fade p-3" id="upload-env" role="tabpanel" aria-labelledby="nav-contact-tab">
<env-file-upload></env-file-upload>
</div>
</div>
<env-keys-modal ref="keysModal"></env-keys-modal>
</div>
@stop
@include('env-editor::components._itemModal')
@include('env-editor::components._currentEnv')
@include('env-editor::components._upload')
@include('env-editor::components._backup')
@include('env-editor::components._configActions')
@push('scripts')
<script>
window.envEventBus = new Vue();
const envAlert = ($type, $text) => {
let alert =
'<div id="__id__" class="alert alert-__type__ alert-dismissible fade show" role="alert">' +
' <div>__text__</div>' +
' <button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
' <span aria-hidden="true">&times;</span>' +
' </button>' +
'</div>';
let $id = 'env-alert_' + Date.now();
let $html = alert.replace('__type__', $type).replace('__text__', $text).replace('__id__', $id);
$('#env-alerts').append($html);
setTimeout(() => {
$('#' + $id).alert('close')
}, 3000)
};
window.envClient = (endpoint, customConfig) => {
const data= customConfig && customConfig.data
let headers = {
'Accept': 'application/json',
"X-CSRF-Token": '{{csrf_token()}}'
}
if (data) {
headers['Content-Type'] = 'application/json'
customConfig.body = JSON.stringify(customConfig.data)
}
const config = {
...customConfig,
headers: headers,
}
return window
.fetch(endpoint, config)
.then(async response => {
const data = await response.json()
if (response.ok) {
return data
}
envAlert('danger', data.message);
return Promise.reject(data)
})
};
const dotEnv = new Vue({
el: '#env-editor',
components: {
'env-main-tab': itemsWrapper,
'env-keys-modal': itemsModal,
'env-file-upload': fileUpload,
'env-editor-backups': backUps,
'env-editor-config-actions': configActions
}
})
</script>
@endpush
@extends('layouts.sidebar')

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="{{app()->getLocale()}}" xml:lang="{{config('app.locale')}}" itemscope itemtype="http://schema.org/WebSite">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@lang('env-editor::env-editor.menuTitle')</title>
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}"/>
<link rel="stylesheet"
href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p"
crossorigin="anonymous"/>
@stack('styles')
</head>
<body>
<span class="javascripts">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF"
crossorigin="anonymous"></script>
@stack('scripts')
</span>
</body>
</html>

View File

@ -0,0 +1,46 @@
#App Settings=Changes settings regarding your LittleLink Custom installation. You probably only want to change the App Name setting.
#=App_Name changes the displayed name for the App in the title, for example.
App_Name="LittleLink Custom"
# You can get a new App Key from https://littlelink-custom.com/key.php
App_Key=base64:YOU+MUST+CHANGE+THIS+YUFukELiN6Bk9gQ19+9zwk=
App_URL=
#Debug Settings=Changes if your page should display a full error description instead of a generic error 500
#=App_debug either true or false. You might want to change this to false after you're done installing, but it's very useful for troubleshooting.
App_debug=true
#=App_env either local or production. Change this to production if you set the value above to false
App_env=local
Log_channel=stack
Log_level=debug
#Database Settings=Should be left alone. If you wish to use mysql you'd have to seed the database again.
DB_connection=sqlite
#Mail Settings=LittleLink Custom comes with a free to use built-in SMTP server for sending mail. You can leave this setting as is, if you wish to use this service please read our terms and conditions at llc-mail.tru.io. If you do not wish to use the built-in SMTP server, change the setting below
#=Mail_mailer either smtp or built-in. Make sure to change this setting if you want to add a custom SMTP server.
Mail_mailer=built-in
Mail_host=
Mail_port=
Mail_username=
Mail_password=
Mail_encryption=
Mail_from_address=
Mail_from_name="${app_name}"
#Cache Settings=Completely optional
Memcached_host=127.0.0.1
Redis_host=127.0.0.1
Redis_password=null
Redis_port=6379
#Miscellaneous Settings=Should be left alone if you don't know what you're doing.
Broadcast_driver=log
Cache_driver=file
Queue_connection=sync
Session_driver=file
Session_lifetime=120