diff --git a/src/.env.example b/src/.env.example
index 746928dc..174e8ce7 100644
--- a/src/.env.example
+++ b/src/.env.example
@@ -1,117 +1,120 @@
APP_NAME=Kolab
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://127.0.0.1:8000
APP_PUBLIC_URL=
APP_DOMAIN=kolabnow.com
+APP_THEME=default
ASSET_URL=http://127.0.0.1:8000
-SUPPORT_URL=
+WEBMAIL_URL=/apps
+SUPPORT_URL=/support
+SUPPORT_EMAIL=
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_DATABASE=kolabdev
DB_HOST=127.0.0.1
DB_PASSWORD=kolab
DB_PORT=3306
DB_USERNAME=kolabdev
BROADCAST_DRIVER=log
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=file
SESSION_LIFETIME=120
MFA_DSN=mysql://roundcube:Welcome2KolabSystems@127.0.0.1/roundcube
MFA_TOTP_DIGITS=6
MFA_TOTP_INTERVAL=30
MFA_TOTP_DIGEST=sha1
IMAP_URI=ssl://127.0.0.1:993
IMAP_ADMIN_LOGIN=cyrus-admin
IMAP_ADMIN_PASSWORD=Welcome2KolabSystems
IMAP_VERIFY_HOST=false
IMAP_VERIFY_PEER=false
LDAP_BASE_DN="dc=mgmt,dc=com"
LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com"
LDAP_HOSTS=127.0.0.1
LDAP_PORT=389
LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com"
LDAP_SERVICE_BIND_PW="Welcome2KolabSystems"
LDAP_USE_SSL=false
LDAP_USE_TLS=false
# Administrative
LDAP_ADMIN_BIND_DN="cn=Directory Manager"
LDAP_ADMIN_BIND_PW="Welcome2KolabSystems"
LDAP_ADMIN_ROOT_DN="dc=mgmt,dc=com"
# Hosted (public registration)
LDAP_HOSTED_BIND_DN="uid=hosted-kolab-service,ou=Special Users,dc=mgmt,dc=com"
LDAP_HOSTED_BIND_PW="Welcome2KolabSystems"
LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com"
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
SWOOLE_HTTP_HOST=127.0.0.1
SWOOLE_HTTP_PORT=8000
PAYMENT_PROVIDER=
MOLLIE_KEY=
STRIPE_KEY=
STRIPE_PUBLIC_KEY=
STRIPE_WEBHOOK_SECRET=
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@example.com"
MAIL_FROM_NAME="Example.com"
MAIL_REPLYTO_ADDRESS=null
MAIL_REPLYTO_NAME=null
DNS_TTL=3600
DNS_SPF="v=spf1 mx -all"
DNS_STATIC="%s. MX 10 ext-mx01.mykolab.com."
DNS_COPY_FROM=null
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
MIX_ASSET_PATH=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
JWT_SECRET=
JWT_TTL=60
COMPANY_NAME=
COMPANY_ADDRESS=
COMPANY_DETAILS=
COMPANY_EMAIL=
COMPANY_LOGO=
COMPANY_FOOTER=
VAT_COUNTRIES=CH,LI
VAT_RATE=7.7
KB_ACCOUNT_DELETE=
KB_ACCOUNT_SUSPENDED=
diff --git a/src/app/Documents/Receipt.php b/src/app/Documents/Receipt.php
index 4c2d1927..a4ba1656 100644
--- a/src/app/Documents/Receipt.php
+++ b/src/app/Documents/Receipt.php
@@ -1,271 +1,276 @@
wallet = $wallet;
$this->year = $year;
$this->month = $month;
}
/**
* Render the mail template with fake data
*
* @param string $type Output format ('html' or 'pdf')
*
* @return string HTML or PDF output
*/
public static function fakeRender(string $type = 'html'): string
{
$wallet = new Wallet();
$wallet->id = \App\Utils::uuidStr();
$wallet->owner = new User(['id' => 123456789]); // @phpstan-ignore-line
$receipt = new self($wallet, date('Y'), date('n'));
self::$fakeMode = true;
if ($type == 'pdf') {
return $receipt->pdfOutput();
} elseif ($type !== 'html') {
throw new \Exception("Unsupported output format");
}
return $receipt->htmlOutput();
}
/**
* Render the receipt in HTML format.
*
* @return string HTML content
*/
public function htmlOutput(): string
{
return $this->build()->render();
}
/**
* Render the receipt in PDF format.
*
* @return string PDF content
*/
public function pdfOutput(): string
{
// Parse ther HTML template
$html = $this->build()->render();
// Link fonts from public/fonts to storage/fonts so DomPdf can find them
if (!is_link(storage_path('fonts/Roboto-Regular.ttf'))) {
symlink(
public_path('fonts/Roboto-Regular.ttf'),
storage_path('fonts/Roboto-Regular.ttf')
);
symlink(
public_path('fonts/Roboto-Bold.ttf'),
storage_path('fonts/Roboto-Bold.ttf')
);
}
// Fix font and image paths
$html = str_replace('url(/fonts/', 'url(fonts/', $html);
- $html = str_replace('src="/images/', 'src="images/', $html);
+ $html = str_replace('src="/', 'src="', $html);
// TODO: The output file is about ~200KB, we could probably slim it down
// by using separate font files with small subset of languages when
// there are no Unicode characters used, e.g. only ASCII or Latin.
// Load PDF generator
$pdf = \PDF::loadHTML($html)->setPaper('a4', 'portrait');
return $pdf->output();
}
/**
* Build the document
*
* @return \Illuminate\View\View The template object
*/
protected function build()
{
$appName = \config('app.name');
$start = Carbon::create($this->year, $this->month, 1, 0, 0, 0);
$end = $start->copy()->endOfMonth();
$month = \trans('documents.month' . intval($this->month));
$title = \trans('documents.receipt-title', ['year' => $this->year, 'month' => $month]);
$company = $this->companyData();
if (self::$fakeMode) {
$country = 'CH';
$customer = [
'id' => $this->wallet->owner->id,
'wallet_id' => $this->wallet->id,
'customer' => 'Freddie Krüger 7252 Westminster Lane Forest Hills, NY 11375',
];
$items = collect([
(object) [
'amount' => 1234,
'updated_at' => $start->copy()->next(Carbon::MONDAY),
],
(object) [
'amount' => 10000,
'updated_at' => $start->copy()->next()->next(),
],
(object) [
'amount' => 1234,
'updated_at' => $start->copy()->next()->next()->next(Carbon::MONDAY),
],
(object) [
'amount' => 99,
'updated_at' => $start->copy()->next()->next()->next(),
],
]);
} else {
$customer = $this->customerData();
$country = $this->wallet->owner->getSetting('country');
$items = $this->wallet->payments()
->where('status', PaymentProvider::STATUS_PAID)
->where('updated_at', '>=', $start)
->where('updated_at', '<', $end)
->where('amount', '<>', 0)
->orderBy('updated_at')
->get();
}
$vatRate = \config('app.vat.rate');
$vatCountries = explode(',', \config('app.vat.countries'));
$vatCountries = array_map('strtoupper', array_map('trim', $vatCountries));
if (!$country || !in_array(strtoupper($country), $vatCountries)) {
$vatRate = 0;
}
$totalVat = 0;
$total = 0;
$items = $items->map(function ($item) use (&$total, &$totalVat, $appName, $vatRate) {
$amount = $item->amount;
if ($vatRate > 0) {
$amount = round($amount * ((100 - $vatRate) / 100));
$totalVat += $item->amount - $amount;
}
$total += $amount;
if ($item->type == PaymentProvider::TYPE_REFUND) {
$description = \trans('documents.receipt-refund');
} elseif ($item->type == PaymentProvider::TYPE_CHARGEBACK) {
$description = \trans('documents.receipt-chargeback');
} else {
$description = \trans('documents.receipt-item-desc', ['site' => $appName]);
}
return [
'amount' => $this->wallet->money($amount),
'description' => $description,
'date' => $item->updated_at->toDateString(),
];
});
// Load the template
$view = view('documents.receipt')
->with([
'site' => $appName,
'title' => $title,
'company' => $company,
'customer' => $customer,
'items' => $items,
'subTotal' => $this->wallet->money($total),
'total' => $this->wallet->money($total + $totalVat),
'totalVat' => $this->wallet->money($totalVat),
'vatRate' => preg_replace('/([.,]00|0|[.,])$/', '', sprintf('%.2f', $vatRate)),
'vat' => $vatRate > 0,
]);
return $view;
}
/**
* Prepare customer data for the template
*
* @return array Customer data for the template
*/
protected function customerData(): array
{
$user = $this->wallet->owner;
$name = $user->name();
$organization = $user->getSetting('organization');
$address = $user->getSetting('billing_address');
$customer = trim(($organization ?: $name) . "\n$address");
$customer = str_replace("\n", ' ', htmlentities($customer));
return [
'id' => $this->wallet->owner->id,
'wallet_id' => $this->wallet->id,
'customer' => $customer,
];
}
/**
* Prepare company data for the template
*
* @return array Company data for the template
*/
protected function companyData(): array
{
$header = \config('app.company.name') . "\n" . \config('app.company.address');
$header = str_replace("\n", ' ', htmlentities($header));
$footerLineLength = 110;
$footer = \config('app.company.details');
$contact = \config('app.company.email');
$logo = \config('app.company.logo');
+ $theme = \config('app.theme');
if ($contact) {
$length = strlen($footer) + strlen($contact) + 3;
$contact = htmlentities($contact);
$footer .= ($length > $footerLineLength ? "\n" : ' | ')
. sprintf('%s', $contact, $contact);
}
+ if ($logo && strpos($logo, '/') === false) {
+ $logo = "/themes/$theme/images/$logo";
+ }
+
return [
- 'logo' => $logo ? "" : '',
+ 'logo' => $logo ? "" : '',
'header' => $header,
'footer' => $footer,
];
}
}
diff --git a/src/app/Http/Controllers/API/V4/SupportController.php b/src/app/Http/Controllers/API/V4/SupportController.php
new file mode 100644
index 00000000..84e7ae9c
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/SupportController.php
@@ -0,0 +1,69 @@
+ 'string|nullable|max:256',
+ 'name' => 'string|nullable|max:256',
+ 'email' => 'required|email',
+ 'summary' => 'required|string|max:512',
+ 'body' => 'required|string',
+ ];
+
+ $params = $request->only(array_keys($rules));
+
+ // Check required fields
+ $v = Validator::make($params, $rules);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ $to = \config('app.support_email');
+
+ if (empty($to)) {
+ \Log::error("Failed to send a support request. SUPPORT_EMAIL not set");
+ return $this->errorResponse(500, \trans('app.support-request-error'));
+ }
+
+ $content = sprintf(
+ "ID: %s\nName: %s\nWorking email address: %s\nSubject: %s\n\n%s\n",
+ $params['user'] ?? '',
+ $params['name'] ?? '',
+ $params['email'],
+ $params['summary'],
+ $params['body'],
+ );
+
+ Mail::raw($content, function ($message) use ($params, $to) {
+ // Remove the global reply-to addressee
+ $message->getHeaders()->remove('Reply-To');
+
+ $message->to($to)
+ ->from($params['email'], $params['name'] ?? null)
+ ->replyTo($params['email'], $params['name'] ?? null)
+ ->subject($params['summary']);
+ });
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans('app.support-request-success'),
+ ]);
+ }
+}
diff --git a/src/app/Http/Controllers/ContentController.php b/src/app/Http/Controllers/ContentController.php
new file mode 100644
index 00000000..7aa099dc
--- /dev/null
+++ b/src/app/Http/Controllers/ContentController.php
@@ -0,0 +1,62 @@
+with('env', \App\Utils::uiEnv());
+ }
+
+ /**
+ * Get the list of FAQ entries for the specified page
+ *
+ * @param string $page Page path
+ *
+ * @return \Illuminate\Http\JsonResponse JSON response
+ */
+ public function faqContent(string $page)
+ {
+ if (empty($page)) {
+ return $this->errorResponse(404);
+ }
+
+ $faq = [];
+
+ $theme_name = \config('app.theme');
+ $theme_file = resource_path("themes/{$theme_name}/theme.json");
+
+ if (file_exists($theme_file)) {
+ $theme = json_decode(file_get_contents($theme_file), true);
+ if (json_last_error() != JSON_ERROR_NONE) {
+ \Log::error("Failed to parse $theme_file: " . json_last_error_msg());
+ } elseif (!empty($theme['faq']) && !empty($theme['faq'][$page])) {
+ $faq = $theme['faq'][$page];
+ }
+
+ // TODO: Support pages with variables, e.g. users/
+ }
+
+ return response()->json(['status' => 'success', 'faq' => $faq]);
+ }
+}
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
index 2fdda4ad..fb97ac97 100644
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -1,52 +1,59 @@
sql, implode(', ', $query->bindings)));
});
}
+
+ // Register some template helpers
+ Blade::directive('theme_asset', function ($path) {
+ $path = trim($path, '/\'"');
+ return "";
+ });
}
}
diff --git a/src/app/Utils.php b/src/app/Utils.php
index 281fe7c8..57200dbc 100644
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -1,338 +1,362 @@
= INET_ATON(?)
ORDER BY INET_ATON(net_number), net_mask DESC LIMIT 1
";
} else {
$query = "
SELECT id FROM ip6nets
WHERE INET6_ATON(net_number) <= INET6_ATON(?)
AND INET6_ATON(net_broadcast) >= INET6_ATON(?)
ORDER BY INET6_ATON(net_number), net_mask DESC LIMIT 1
";
}
$nets = \Illuminate\Support\Facades\DB::select($query, [$ip, $ip]);
if (sizeof($nets) > 0) {
return $nets[0]->country;
}
return 'CH';
}
/**
* Return the country ISO code for the current request.
*/
public static function countryForRequest()
{
$request = \request();
$ip = $request->ip();
return self::countryForIP($ip);
}
/**
* Shortcut to creating a progress bar of a particular format with a particular message.
*
* @param \Illuminate\Console\OutputStyle $output Console output object
* @param int $count Number of progress steps
* @param string $message The description
*
* @return \Symfony\Component\Console\Helper\ProgressBar
*/
public static function createProgressBar($output, $count, $message = null)
{
$bar = $output->createProgressBar($count);
$bar->setFormat(
'%current:7s%/%max:7s% [%bar%] %percent:3s%% %elapsed:7s%/%estimated:-7s% %message% '
);
if ($message) {
$bar->setMessage($message . " ...");
}
$bar->start();
return $bar;
}
/**
* Return the number of days in the month prior to this one.
*
* @return int
*/
public static function daysInLastMonth()
{
$start = new Carbon('first day of last month');
$end = new Carbon('last day of last month');
return $start->diffInDays($end) + 1;
}
/**
* Download a file from the interwebz and store it locally.
*
* @param string $source The source location
* @param string $target The target location
* @param bool $force Force the download (and overwrite target)
*
* @return void
*/
public static function downloadFile($source, $target, $force = false)
{
if (is_file($target) && !$force) {
return;
}
\Log::info("Retrieving {$source}");
$fp = fopen($target, 'w');
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $source);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_FILE, $fp);
curl_exec($curl);
if (curl_errno($curl)) {
\Log::error("Request error on {$source}: " . curl_error($curl));
curl_close($curl);
fclose($fp);
unlink($target);
return;
}
curl_close($curl);
fclose($fp);
}
/**
* Calculate the broadcast address provided a net number and a prefix.
*
* @param string $net A valid IPv6 network number.
* @param int $prefix The network prefix.
*
* @return string
*/
public static function ip6Broadcast($net, $prefix)
{
$netHex = bin2hex(inet_pton($net));
// Overwriting first address string to make sure notation is optimal
$net = inet_ntop(hex2bin($netHex));
// Calculate the number of 'flexible' bits
$flexbits = 128 - $prefix;
// Build the hexadecimal string of the last address
$lastAddrHex = $netHex;
// We start at the end of the string (which is always 32 characters long)
$pos = 31;
while ($flexbits > 0) {
// Get the character at this position
$orig = substr($lastAddrHex, $pos, 1);
// Convert it to an integer
$origval = hexdec($orig);
// OR it with (2^flexbits)-1, with flexbits limited to 4 at a time
$newval = $origval | (pow(2, min(4, $flexbits)) - 1);
// Convert it back to a hexadecimal character
$new = dechex($newval);
// And put that character back in the string
$lastAddrHex = substr_replace($lastAddrHex, $new, $pos, 1);
// We processed one nibble, move to previous position
$flexbits -= 4;
$pos -= 1;
}
// Convert the hexadecimal string to a binary string
# Using pack() here
# Newer PHP version can use hex2bin()
$lastaddrbin = pack('H*', $lastAddrHex);
// And create an IPv6 address from the binary string
$lastaddrstr = inet_ntop($lastaddrbin);
return $lastaddrstr;
}
/**
* Provide all unique combinations of elements in $input, with order and duplicates irrelevant.
*
* @param array $input The input array of elements.
*
* @return array[]
*/
public static function powerSet(array $input): array
{
$output = [];
for ($x = 0; $x < count($input); $x++) {
self::combine($input, $x + 1, 0, [], 0, $output);
}
return $output;
}
/**
* Returns the current user's email address or null.
*
* @return string
*/
public static function userEmailOrNull(): ?string
{
$user = Auth::user();
if (!$user) {
return null;
}
return $user->email;
}
/**
* Returns a UUID in the form of an integer.
*
* @return integer
*/
public static function uuidInt(): int
{
$hex = Uuid::uuid4();
$bin = pack('h*', str_replace('-', '', $hex));
$ids = unpack('L', $bin);
$id = array_shift($ids);
return $id;
}
/**
* Returns a UUID in the form of a string.
*
* @return string
*/
public static function uuidStr(): string
{
return Uuid::uuid4()->toString();
}
private static function combine($input, $r, $index, $data, $i, &$output): void
{
$n = count($input);
// Current cobination is ready
if ($index == $r) {
$output[] = array_slice($data, 0, $r);
return;
}
// When no more elements are there to put in data[]
if ($i >= $n) {
return;
}
// current is included, put next at next location
$data[$index] = $input[$i];
self::combine($input, $r, $index + 1, $data, $i + 1, $output);
// current is excluded, replace it with next (Note that i+1
// is passed, but index is not changed)
self::combine($input, $r, $index, $data, $i + 1, $output);
}
/**
* Create self URL
*
* @param string $route Route/Path
*
* @return string Full URL
*/
public static function serviceUrl(string $route): string
{
$url = \config('app.public_url');
if (!$url) {
$url = \config('app.url');
}
return rtrim(trim($url, '/') . '/' . ltrim($route, '/'), '/');
}
/**
* Create a configuration/environment data to be passed to
* the UI
*
* @todo For a lack of better place this is put here for now
*
* @return array Configuration data
*/
public static function uiEnv(): array
{
- $opts = ['app.name', 'app.url', 'app.domain'];
+ $opts = [
+ 'app.name',
+ 'app.url',
+ 'app.domain',
+ 'app.theme',
+ 'app.webmail_url',
+ 'app.support_email',
+ 'mail.from.address'
+ ];
+
$env = \app('config')->getMany($opts);
$countries = include resource_path('countries.php');
$env['countries'] = $countries ?: [];
$isAdmin = strpos(request()->getHttpHost(), 'admin.') === 0;
$env['jsapp'] = $isAdmin ? 'admin.js' : 'user.js';
$env['paymentProvider'] = \config('services.payment_provider');
$env['stripePK'] = \config('services.stripe.public_key');
+ $theme_file = resource_path("themes/{$env['app.theme']}/theme.json");
+ $menu = [];
+
+ if (file_exists($theme_file)) {
+ $theme = json_decode(file_get_contents($theme_file), true);
+
+ if (json_last_error() != JSON_ERROR_NONE) {
+ \Log::error("Failed to parse $theme_file: " . json_last_error_msg());
+ } elseif (!empty($theme['menu'])) {
+ $menu = $theme['menu'];
+ }
+ }
+
+ $env['menu'] = $menu;
+
return $env;
}
}
diff --git a/src/config/app.php b/src/config/app.php
index e8c1be22..79162361 100644
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -1,262 +1,268 @@
env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
*/
'url' => env('APP_URL', 'http://localhost'),
'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')),
'asset_url' => env('ASSET_URL', null),
'support_url' => env('SUPPORT_URL', null),
+ 'support_email' => env('SUPPORT_EMAIL', null),
+
+ 'webmail_url' => env('WEBMAIL_URL', null),
+
+ 'theme' => env('APP_THEME', 'default'),
+
/*
|--------------------------------------------------------------------------
| Application Domain
|--------------------------------------------------------------------------
|
| System domain used for user signup (kolab identity)
*/
'domain' => env('APP_DOMAIN', 'domain.tld'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
Barryvdh\DomPDF\ServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
// Locations of knowledge base articles
'kb' => [
// An article about suspended accounts
'account_suspended' => env('KB_ACCOUNT_SUSPENDED'),
// An article about a way to delete an owned account
'account_delete' => env('KB_ACCOUNT_DELETE'),
],
'company' => [
'name' => env('COMPANY_NAME'),
'address' => env('COMPANY_ADDRESS'),
'details' => env('COMPANY_DETAILS'),
'email' => env('COMPANY_EMAIL'),
'logo' => env('COMPANY_LOGO'),
'footer' => env('COMPANY_FOOTER', env('COMPANY_DETAILS')),
],
'vat' => [
'countries' => env('VAT_COUNTRIES'),
'rate' => (float) env('VAT_RATE'),
],
];
diff --git a/src/config/view.php b/src/config/view.php
index 22b8a18d..de84cc3c 100644
--- a/src/config/view.php
+++ b/src/config/view.php
@@ -1,36 +1,37 @@
[
resource_path('views'),
+ resource_path('themes'),
],
/*
|--------------------------------------------------------------------------
| Compiled View Path
|--------------------------------------------------------------------------
|
| This option determines where all the compiled Blade templates will be
| stored for your application. Typically, this is within the storage
| directory. However, as usual, you are free to change this value.
|
*/
'compiled' => env(
'VIEW_COMPILED_PATH',
realpath(storage_path('framework/views'))
),
];
diff --git a/src/public/images/icons/icon-128x128.png b/src/public/images/icons/icon-128x128.png
deleted file mode 100644
index cf4f4aaa..00000000
Binary files a/src/public/images/icons/icon-128x128.png and /dev/null differ
diff --git a/src/public/images/icons/icon-144x144.png b/src/public/images/icons/icon-144x144.png
deleted file mode 100644
index ce871ea1..00000000
Binary files a/src/public/images/icons/icon-144x144.png and /dev/null differ
diff --git a/src/public/images/icons/icon-152x152.png b/src/public/images/icons/icon-152x152.png
deleted file mode 100644
index fcae506a..00000000
Binary files a/src/public/images/icons/icon-152x152.png and /dev/null differ
diff --git a/src/public/images/icons/icon-192x192.png b/src/public/images/icons/icon-192x192.png
deleted file mode 100644
index 536a8d20..00000000
Binary files a/src/public/images/icons/icon-192x192.png and /dev/null differ
diff --git a/src/public/images/icons/icon-384x384.png b/src/public/images/icons/icon-384x384.png
deleted file mode 100644
index 4573d9dd..00000000
Binary files a/src/public/images/icons/icon-384x384.png and /dev/null differ
diff --git a/src/public/images/icons/icon-512x512.png b/src/public/images/icons/icon-512x512.png
deleted file mode 100644
index 5e6748ed..00000000
Binary files a/src/public/images/icons/icon-512x512.png and /dev/null differ
diff --git a/src/public/images/icons/icon-72x72.png b/src/public/images/icons/icon-72x72.png
deleted file mode 100644
index 8d40cd59..00000000
Binary files a/src/public/images/icons/icon-72x72.png and /dev/null differ
diff --git a/src/public/images/icons/icon-96x96.png b/src/public/images/icons/icon-96x96.png
deleted file mode 100644
index 0db2f3f1..00000000
Binary files a/src/public/images/icons/icon-96x96.png and /dev/null differ
diff --git a/src/public/images/icons/splash-1125x2436.png b/src/public/images/icons/splash-1125x2436.png
deleted file mode 100644
index 05b019ad..00000000
Binary files a/src/public/images/icons/splash-1125x2436.png and /dev/null differ
diff --git a/src/public/images/icons/splash-1242x2208.png b/src/public/images/icons/splash-1242x2208.png
deleted file mode 100644
index 127202e2..00000000
Binary files a/src/public/images/icons/splash-1242x2208.png and /dev/null differ
diff --git a/src/public/images/icons/splash-1242x2688.png b/src/public/images/icons/splash-1242x2688.png
deleted file mode 100644
index dec52b6a..00000000
Binary files a/src/public/images/icons/splash-1242x2688.png and /dev/null differ
diff --git a/src/public/images/icons/splash-1536x2048.png b/src/public/images/icons/splash-1536x2048.png
deleted file mode 100644
index 2008c757..00000000
Binary files a/src/public/images/icons/splash-1536x2048.png and /dev/null differ
diff --git a/src/public/images/icons/splash-1668x2224.png b/src/public/images/icons/splash-1668x2224.png
deleted file mode 100644
index 7fac0728..00000000
Binary files a/src/public/images/icons/splash-1668x2224.png and /dev/null differ
diff --git a/src/public/images/icons/splash-1668x2388.png b/src/public/images/icons/splash-1668x2388.png
deleted file mode 100644
index f39d2ceb..00000000
Binary files a/src/public/images/icons/splash-1668x2388.png and /dev/null differ
diff --git a/src/public/images/icons/splash-2048x2732.png b/src/public/images/icons/splash-2048x2732.png
deleted file mode 100644
index 3df934df..00000000
Binary files a/src/public/images/icons/splash-2048x2732.png and /dev/null differ
diff --git a/src/public/images/icons/splash-640x1136.png b/src/public/images/icons/splash-640x1136.png
deleted file mode 100644
index 73a34ad0..00000000
Binary files a/src/public/images/icons/splash-640x1136.png and /dev/null differ
diff --git a/src/public/images/icons/splash-750x1334.png b/src/public/images/icons/splash-750x1334.png
deleted file mode 100644
index a38e09d6..00000000
Binary files a/src/public/images/icons/splash-750x1334.png and /dev/null differ
diff --git a/src/public/images/icons/splash-828x1792.png b/src/public/images/icons/splash-828x1792.png
deleted file mode 100644
index f6de06c2..00000000
Binary files a/src/public/images/icons/splash-828x1792.png and /dev/null differ
diff --git a/src/public/mix-manifest.json b/src/public/mix-manifest.json
deleted file mode 100644
index f452e347..00000000
--- a/src/public/mix-manifest.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "/js/admin.js": "/js/admin.js",
- "/js/user.js": "/js/user.js",
- "/css/app.css": "/css/app.css",
- "/css/document.css": "/css/document.css"
-}
diff --git a/src/readme.md b/src/readme.md
deleted file mode 100644
index f95b2ec9..00000000
--- a/src/readme.md
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
-
-
-
-
-
-## About Laravel
-
-Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
-
-- [Simple, fast routing engine](https://laravel.com/docs/routing).
-- [Powerful dependency injection container](https://laravel.com/docs/container).
-- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
-- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
-- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
-- [Robust background job processing](https://laravel.com/docs/queues).
-- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
-
-Laravel is accessible, powerful, and provides tools required for large, robust applications.
-
-## Learning Laravel
-
-Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
-
-If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 1500 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
-
-## Laravel Sponsors
-
-We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
-
-- **[Vehikl](https://vehikl.com/)**
-- **[Tighten Co.](https://tighten.co)**
-- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
-- **[64 Robots](https://64robots.com)**
-- **[Cubet Techno Labs](https://cubettech.com)**
-- **[Cyber-Duck](https://cyber-duck.co.uk)**
-- **[British Software Development](https://www.britishsoftware.co)**
-- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
-- **[DevSquad](https://devsquad.com)**
-- [UserInsights](https://userinsights.com)
-- [Fragrantica](https://www.fragrantica.com)
-- [SOFTonSOFA](https://softonsofa.com/)
-- [User10](https://user10.com)
-- [Soumettre.fr](https://soumettre.fr/)
-- [CodeBrisk](https://codebrisk.com)
-- [1Forge](https://1forge.com)
-- [TECPRESSO](https://tecpresso.co.jp/)
-- [Runtime Converter](http://runtimeconverter.com/)
-- [WebL'Agence](https://weblagence.com/)
-- [Invoice Ninja](https://www.invoiceninja.com)
-- [iMi digital](https://www.imi-digital.de/)
-- [Earthlink](https://www.earthlink.ro/)
-- [Steadfast Collective](https://steadfastcollective.com/)
-- [We Are The Robots Inc.](https://watr.mx/)
-- [Understand.io](https://www.understand.io/)
-- [Abdel Elrafa](https://abdelelrafa.com)
-- [Hyper Host](https://hyper.host)
-
-## Contributing
-
-Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
-
-## Security Vulnerabilities
-
-If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
-
-## License
-
-The Laravel framework is open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT).
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
index d9d439e9..a88421c5 100644
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -1,358 +1,414 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap')
import AppComponent from '../vue/App'
import MenuComponent from '../vue/Widgets/Menu'
+import SupportForm from '../vue/Widgets/SupportForm'
import store from './store'
const loader = '
Loading
'
const app = new Vue({
el: '#app',
components: {
AppComponent,
MenuComponent,
},
store,
router: window.router,
data() {
return {
isLoading: true,
isAdmin: window.isAdmin
}
},
methods: {
// Clear (bootstrap) form validation state
clearFormValidation(form) {
$(form).find('.is-invalid').removeClass('is-invalid')
$(form).find('.invalid-feedback').remove()
},
isController(wallet_id) {
if (wallet_id && store.state.authInfo) {
let i
for (i = 0; i < store.state.authInfo.wallets.length; i++) {
if (wallet_id == store.state.authInfo.wallets[i].id) {
return true
}
}
for (i = 0; i < store.state.authInfo.accounts.length; i++) {
if (wallet_id == store.state.authInfo.accounts[i].id) {
return true
}
}
}
return false
},
// Set user state to "logged in"
loginUser(response, dashboard, update) {
if (!update) {
store.commit('logoutUser') // destroy old state data
store.commit('loginUser')
}
localStorage.setItem('token', response.access_token)
axios.defaults.headers.common.Authorization = 'Bearer ' + response.access_token
if (response.email) {
store.state.authInfo = response
}
if (dashboard !== false) {
this.$router.push(store.state.afterLogin || { name: 'dashboard' })
}
store.state.afterLogin = null
// Refresh the token before it expires
let timeout = response.expires_in || 0
// We'll refresh 60 seconds before the token expires
if (timeout > 60) {
timeout -= 60
}
// TODO: We probably should try a few times in case of an error
// TODO: We probably should prevent axios from doing any requests
// while the token is being refreshed
this.refreshTimeout = setTimeout(() => {
axios.post('/api/auth/refresh').then(response => {
this.loginUser(response.data, false, true)
})
}, timeout * 1000)
},
// Set user state to "not logged in"
logoutUser() {
store.commit('logoutUser')
localStorage.setItem('token', '')
delete axios.defaults.headers.common.Authorization
this.$router.push({ name: 'login' })
clearTimeout(this.refreshTimeout)
},
// Display "loading" overlay inside of the specified element
addLoader(elem) {
$(elem).css({position: 'relative'}).append($(loader).addClass('small'))
},
// Remove loader element added in addLoader()
removeLoader(elem) {
$(elem).find('.app-loader').remove()
},
startLoading() {
this.isLoading = true
// Lock the UI with the 'loading...' element
let loading = $('#app > .app-loader').removeClass('fadeOut')
if (!loading.length) {
$('#app').append($(loader))
}
},
// Hide "loading" overlay
stopLoading() {
$('#app > .app-loader').addClass('fadeOut')
this.isLoading = false
},
errorPage(code, msg) {
// Until https://github.com/vuejs/vue-router/issues/977 is implemented
// we can't really use router to display error page as it has two side
// effects: it changes the URL and adds the error page to browser history.
// For now we'll be replacing current view with error page "manually".
const map = {
400: "Bad request",
401: "Unauthorized",
403: "Access denied",
404: "Not found",
405: "Method not allowed",
500: "Internal server error"
}
if (!msg) msg = map[code] || "Unknown Error"
- const error_page = `
${code}
${msg}
`
+ const error_page = `
${code}
${msg}
`
$('#error-page').remove()
$('#app').append(error_page)
+
+ app.updateBodyClass('error')
},
errorHandler(error) {
this.stopLoading()
if (!error.response) {
// TODO: probably network connection error
} else if (error.response.status === 401) {
// Remember requested route to come back to it after log in
if (this.$route.meta.requiresAuth) {
store.state.afterLogin = this.$route
}
this.logoutUser()
} else {
this.errorPage(error.response.status, error.response.statusText)
}
},
downloadFile(url) {
// TODO: This might not be a best way for big files as the content
// will be stored (temporarily) in browser memory
// TODO: This method does not show the download progress in the browser
// but it could be implemented in the UI, axios has 'progress' property
axios.get(url, { responseType: 'blob' })
.then (response => {
const link = document.createElement('a')
const contentDisposition = response.headers['content-disposition']
let filename = 'unknown'
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+)"/);
if (match.length === 2) {
filename = match[1];
}
}
link.href = window.URL.createObjectURL(response.data)
link.download = filename
link.click()
})
},
price(price, currency) {
return ((price || 0) / 100).toLocaleString('de-DE', { style: 'currency', currency: currency || 'CHF' })
},
priceLabel(cost, units = 1, discount) {
let index = ''
if (units < 0) {
units = 1
}
if (discount) {
cost = Math.floor(cost * ((100 - discount) / 100))
index = '\u00B9'
}
return this.price(cost * units) + '/month' + index
},
clickRecord(event) {
if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) {
let link = $(event.target).closest('tr').find('a')[0]
if (link) {
link.click()
}
}
},
domainStatusClass(domain) {
if (domain.isDeleted) {
return 'text-muted'
}
if (domain.isSuspended) {
return 'text-warning'
}
if (!domain.isVerified || !domain.isLdapReady || !domain.isConfirmed) {
return 'text-danger'
}
return 'text-success'
},
domainStatusText(domain) {
if (domain.isDeleted) {
return 'Deleted'
}
if (domain.isSuspended) {
return 'Suspended'
}
if (!domain.isVerified || !domain.isLdapReady || !domain.isConfirmed) {
return 'Not Ready'
}
return 'Active'
},
+ pageName(path) {
+ let page = this.$route.path
+
+ // check if it is a "menu page", find the page name
+ // otherwise we'll use the real path as page name
+ window.config.menu.every(item => {
+ if (item.location == page && item.page) {
+ page = item.page
+ return false
+ }
+ })
+
+ page = page.replace(/^\//, '')
+
+ return page ? page : '404'
+ },
+ supportDialog(container) {
+ let dialog = $('#support-dialog')
+
+ // FIXME: Find a nicer way of doing this
+ if (!dialog.length) {
+ let form = new Vue(SupportForm)
+ form.$mount($('
').appendTo(container)[0])
+ form.$root = this
+ form.$toast = this.$toast
+ dialog = $(form.$el)
+ }
+
+ dialog.on('shown.bs.modal', () => {
+ dialog.find('input').first().focus()
+ }).modal()
+ },
userStatusClass(user) {
if (user.isDeleted) {
return 'text-muted'
}
if (user.isSuspended) {
return 'text-warning'
}
if (!user.isImapReady || !user.isLdapReady) {
return 'text-danger'
}
return 'text-success'
},
userStatusText(user) {
if (user.isDeleted) {
return 'Deleted'
}
if (user.isSuspended) {
return 'Suspended'
}
if (!user.isImapReady || !user.isLdapReady) {
return 'Not Ready'
}
return 'Active'
+ },
+ updateBodyClass(name) {
+ // Add 'class' attribute to the body, different for each page
+ // so, we can apply page-specific styles
+ let className = 'page-' + (name || this.pageName()).replace(/\/.*$/, '')
+ $(document.body).removeClass().addClass(className)
}
}
})
// Add a axios request interceptor
window.axios.interceptors.request.use(
config => {
// This is the only way I found to change configuration options
// on a running application. We need this for browser testing.
config.headers['X-Test-Payment-Provider'] = window.config.paymentProvider
return config
},
error => {
// Do something with request error
return Promise.reject(error)
}
)
// Add a axios response interceptor for general/validation error handler
window.axios.interceptors.response.use(
response => {
// Do nothing
return response
},
error => {
let error_msg
let status = error.response ? error.response.status : 200
+ // Do not display the error in a toast message, pass the error as-is
+ if (error.config.ignoreErrors) {
+ return Promise.reject(error)
+ }
+
if (error.response && status == 422) {
error_msg = "Form validation error"
const modal = $('div.modal.show')
$(modal.length ? modal : 'form').each((i, form) => {
form = $(form)
$.each(error.response.data.errors || {}, (idx, msg) => {
const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx
let input = form.find('#' + input_name)
if (!input.length) {
input = form.find('[name="' + input_name + '"]');
}
if (input.length) {
// Create an error message\
// API responses can use a string, array or object
let msg_text = ''
if ($.type(msg) !== 'string') {
$.each(msg, (index, str) => {
msg_text += str + ' '
})
}
else {
msg_text = msg
}
let feedback = $('
').text(msg_text)
if (input.is('.list-input')) {
// List input widget
input.children(':not(:first-child)').each((index, element) => {
if (msg[index]) {
$(element).find('input').addClass('is-invalid')
}
})
input.addClass('is-invalid').next('.invalid-feedback').remove()
input.after(feedback)
}
else {
// Standard form element
input.addClass('is-invalid')
input.parent().find('.invalid-feedback').remove()
input.parent().append(feedback)
}
}
})
form.find('.is-invalid:not(.listinput-widget)').first().focus()
})
}
else if (error.response && error.response.data) {
error_msg = error.response.data.message
}
else {
error_msg = error.request ? error.request.statusText : error.message
}
app.$toast.error(error_msg || "Server Error")
// Pass the error as-is
return Promise.reject(error)
}
)
+
+// TODO: Investigate if we can use App component's childMounted() method instead
+window.router.afterEach((to, from) => {
+ // When changing a page remove old:
+ // - error page
+ // - modal backdrop
+ $('#error-page,.modal-backdrop.show').remove()
+
+ app.updateBodyClass()
+})
diff --git a/src/resources/js/bootstrap.js b/src/resources/js/bootstrap.js
index 88b4aecd..d12f5c26 100644
--- a/src/resources/js/bootstrap.js
+++ b/src/resources/js/bootstrap.js
@@ -1,101 +1,94 @@
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
window.Popper = require('popper.js').default
window.$ = window.jQuery = require('jquery')
require('bootstrap')
/**
* We'll load Vue, VueRouter and global components
*/
import FontAwesomeIcon from './fontawesome'
import VueRouter from 'vue-router'
import Toast from '../vue/Widgets/Toast'
import store from './store'
window.Vue = require('vue')
Vue.component('svg-icon', FontAwesomeIcon)
const vTooltip = (el, binding) => {
const t = []
if (binding.modifiers.focus) t.push('focus')
if (binding.modifiers.hover) t.push('hover')
if (binding.modifiers.click) t.push('click')
if (!t.length) t.push('hover')
$(el).tooltip({
title: binding.value,
placement: binding.arg || 'top',
trigger: t.join(' '),
html: !!binding.modifiers.html,
});
}
Vue.directive('tooltip', {
bind: vTooltip,
update: vTooltip,
unbind (el) {
$(el).tooltip('dispose')
}
})
Vue.use(Toast)
Vue.use(VueRouter)
let vueRouterBase = '/'
try {
let url = new URL(window.config['app.url'])
vueRouterBase = url.pathname
} catch(e) {
// ignore
}
window.router = new VueRouter({
base: vueRouterBase,
mode: 'history',
routes: window.routes,
scrollBehavior (to, from, savedPosition) {
// Scroll the page to top, but not on Back action
return savedPosition || { x: 0, y: 0 }
}
})
router.beforeEach((to, from, next) => {
// check if the route requires authentication and user is not logged in
if (to.matched.some(route => route.meta.requiresAuth) && !store.state.isLoggedIn) {
// remember the original request, to use after login
store.state.afterLogin = to;
// redirect to login page
next({ name: 'login' })
return
}
next()
})
-router.afterEach((to, from) => {
- // When changing a page remove old:
- // - error page
- // - modal backdrop
- $('#error-page,.modal-backdrop.show').remove()
-})
-
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios')
axios.defaults.baseURL = vueRouterBase
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js
index 318a8da5..0ee06731 100644
--- a/src/resources/js/fontawesome.js
+++ b/src/resources/js/fontawesome.js
@@ -1,55 +1,57 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
//import { } from '@fortawesome/free-brands-svg-icons'
import {
faCheckSquare,
faCreditCard,
faSquare,
} from '@fortawesome/free-regular-svg-icons'
import {
faCheck,
faCheckCircle,
faDownload,
+ faEnvelope,
faGlobe,
faExclamationCircle,
faInfoCircle,
faLock,
faKey,
faPlus,
faSearch,
faSignInAlt,
faSyncAlt,
faTrashAlt,
faUser,
faUserCog,
faUsers,
faWallet
} from '@fortawesome/free-solid-svg-icons'
// Register only these icons we need
library.add(
faCheck,
faCheckCircle,
faCheckSquare,
faCreditCard,
faDownload,
+ faEnvelope,
faExclamationCircle,
faGlobe,
faInfoCircle,
faLock,
faKey,
faPlus,
faSearch,
faSignInAlt,
faSquare,
faSyncAlt,
faTrashAlt,
faUser,
faUserCog,
faUsers,
faWallet
)
export default FontAwesomeIcon
diff --git a/src/resources/js/routes-admin.js b/src/resources/js/routes-admin.js
index 27665db2..c34aa428 100644
--- a/src/resources/js/routes-admin.js
+++ b/src/resources/js/routes-admin.js
@@ -1,48 +1,48 @@
import DashboardComponent from '../vue/Admin/Dashboard'
import DomainComponent from '../vue/Admin/Domain'
-import Error404Component from '../vue/404'
import LoginComponent from '../vue/Login'
import LogoutComponent from '../vue/Logout'
+import PageComponent from '../vue/Page'
import UserComponent from '../vue/Admin/User'
const routes = [
{
path: '/',
redirect: { name: 'dashboard' }
},
{
path: '/dashboard',
name: 'dashboard',
component: DashboardComponent,
meta: { requiresAuth: true }
},
{
path: '/domain/:domain',
name: 'domain',
component: DomainComponent,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'login',
component: LoginComponent
},
{
path: '/logout',
name: 'logout',
component: LogoutComponent
},
{
path: '/user/:user',
name: 'user',
component: UserComponent,
meta: { requiresAuth: true }
},
{
name: '404',
path: '*',
- component: Error404Component
+ component: PageComponent
}
]
export default routes
diff --git a/src/resources/js/routes-user.js b/src/resources/js/routes-user.js
index 1612aaac..9b0dc774 100644
--- a/src/resources/js/routes-user.js
+++ b/src/resources/js/routes-user.js
@@ -1,96 +1,92 @@
import DashboardComponent from '../vue/Dashboard'
import DomainInfoComponent from '../vue/Domain/Info'
import DomainListComponent from '../vue/Domain/List'
-import Error404Component from '../vue/404'
import LoginComponent from '../vue/Login'
import LogoutComponent from '../vue/Logout'
+import PageComponent from '../vue/Page'
import PasswordResetComponent from '../vue/PasswordReset'
import SignupComponent from '../vue/Signup'
import UserInfoComponent from '../vue/User/Info'
import UserListComponent from '../vue/User/List'
import UserProfileComponent from '../vue/User/Profile'
import UserProfileDeleteComponent from '../vue/User/ProfileDelete'
import WalletComponent from '../vue/Wallet'
const routes = [
- {
- path: '/',
- redirect: { name: 'dashboard' }
- },
{
path: '/dashboard',
name: 'dashboard',
component: DashboardComponent,
meta: { requiresAuth: true }
},
{
path: '/domain/:domain',
name: 'domain',
component: DomainInfoComponent,
meta: { requiresAuth: true }
},
{
path: '/domains',
name: 'domains',
component: DomainListComponent,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'login',
component: LoginComponent
},
{
path: '/logout',
name: 'logout',
component: LogoutComponent
},
{
path: '/password-reset/:code?',
name: 'password-reset',
component: PasswordResetComponent
},
{
path: '/profile',
name: 'profile',
component: UserProfileComponent,
meta: { requiresAuth: true }
},
{
path: '/profile/delete',
name: 'profile-delete',
component: UserProfileDeleteComponent,
meta: { requiresAuth: true }
},
{
path: '/signup/:param?',
alias: '/signup/voucher/:param',
name: 'signup',
component: SignupComponent
},
{
path: '/user/:user',
name: 'user',
component: UserInfoComponent,
meta: { requiresAuth: true }
},
{
path: '/users',
name: 'users',
component: UserListComponent,
meta: { requiresAuth: true }
},
{
path: '/wallet',
name: 'wallet',
component: WalletComponent,
meta: { requiresAuth: true }
},
{
name: '404',
path: '*',
- component: Error404Component
+ component: PageComponent
}
]
export default routes
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
index 76974430..97790828 100644
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -1,56 +1,59 @@
'The auto-payment has been removed.',
'mandate-update-success' => 'The auto-payment has been updated.',
'planbutton' => 'Choose :plan',
'process-user-new' => 'Registering a user...',
'process-user-ldap-ready' => 'Creating a user...',
'process-user-imap-ready' => 'Creating a mailbox...',
'process-domain-new' => 'Registering a custom domain...',
'process-domain-ldap-ready' => 'Creating a custom domain...',
'process-domain-verified' => 'Verifying a custom domain...',
'process-domain-confirmed' => 'Verifying an ownership of a custom domain...',
'process-success' => 'Setup process finished successfully.',
'process-error-user-ldap-ready' => 'Failed to create a user.',
'process-error-user-imap-ready' => 'Failed to verify that a mailbox exists.',
'process-error-domain-ldap-ready' => 'Failed to create a domain.',
'process-error-domain-verified' => 'Failed to verify a domain.',
'process-error-domain-confirmed' => 'Failed to verify an ownership of a domain.',
'domain-verify-success' => 'Domain verified successfully.',
'domain-verify-error' => 'Domain ownership verification failed.',
'domain-suspend-success' => 'Domain suspended successfully.',
'domain-unsuspend-success' => 'Domain unsuspended successfully.',
'user-update-success' => 'User data updated successfully.',
'user-create-success' => 'User created successfully.',
'user-delete-success' => 'User deleted successfully.',
'user-suspend-success' => 'User suspended successfully.',
'user-unsuspend-success' => 'User unsuspended successfully.',
'user-reset-2fa-success' => '2-Factor authentication reset successfully.',
'search-foundxdomains' => ':x domains have been found.',
'search-foundxusers' => ':x user accounts have been found.',
+ 'support-request-success' => 'Support request submitted successfully.',
+ 'support-request-error' => 'Failed to submit the support request.',
+
'wallet-award-success' => 'The bonus has been added to the wallet successfully.',
'wallet-penalty-success' => 'The penalty has been added to the wallet successfully.',
'wallet-update-success' => 'User wallet updated successfully.',
'wallet-notice-date' => 'With your current subscriptions your account balance will last until about :date (:days).',
'wallet-notice-nocredit' => 'You are out of credit, top up your balance now.',
'wallet-notice-today' => 'You will run out of credit today, top up your balance now.',
'wallet-notice-trial' => 'You are in your free trial period.',
'wallet-notice-trial-end' => 'Your free trial is about to end, top up to continue.',
];
diff --git a/src/resources/themes/README.md b/src/resources/themes/README.md
new file mode 100644
index 00000000..3a07c189
--- /dev/null
+++ b/src/resources/themes/README.md
@@ -0,0 +1,65 @@
+## THEMES
+
+### Creating a theme
+
+1. First create the theme directory and content by copying the default theme:
+
+```
+cp resources/themes/default resources/themes/mytheme
+```
+
+2. Compile resources. This will also make sure to copy static files (e.g. images)
+ to `public/themes/`:
+
+```
+npm run prod
+```
+
+3. Configure the app to use your new theme (in .env file):
+
+```
+APP_THEME=mytheme
+```
+
+### Styles
+
+The main theme directory should include following files:
+
+- "theme.json": Theme metadata, e.g. menu definition.
+- "app.scss": The app styles.
+- "document.scss": Documents styles.
+- "images/logo_header.png": An image that is not controlled by the theme (yet).
+- "images/logo_footer.png": An image that is not controlled by the theme (yet).
+- "images/favicon.ico": An image that is not controlled by the theme (yet).
+
+Note: Applying some styles to `` or other elements outside of the template
+content can be done using `.page-` class that is always added to the ``.
+
+### Menu definition
+
+The menu items are defined using "menu" property in `theme.json` file.
+It should be an array of object. Here are all available properties for such an object.
+
+- "title" (string): The displayed label for the menu item. Required.
+- "location" (string): The page location. Can be a full URL (for external pages)
+ or relative path starting with a slash for internal locations. Required.
+- "page" (string): The name of the page. Required for internal pages.
+ This is the first element of the page template file which should exist
+ in `resources/themes//pages/` directory. The template file name should be
+ `.blade.php`.
+- "footer" (bool): Whether the menu should appear only in the footer menu.
+
+Note that menu definition should not include special pages like "Signup", "Contact" or "Login".
+
+### Page templates
+
+Page content templates placed in `resources/themes//pages/` directory are
+Blade templates. Some notes about that:
+
+- the content will be placed inside the page layout so you should not use nor
+ nor even a wrapper
+ Our technical support team is here to provide help, should you run
+ into issues. You won’t have to talk to computers or navigate voice
+ menus, but have actual human beings answering you personally.
+
+
+ This support is already included in your subscription fee, so
+ there are no further costs for you. If you have issues with your
+ Kolab Now account, or questions about our product before you sign
+ up, please contact us.
+
- Enter your email address to reset your password. You may need to check your spam folder or unblock noreply@kolabnow.com.
+ Enter your email address to reset your password.
+ You may need to check your spam folder or unblock {{ fromEmail }}.
Password Reset - Step 2/3
We sent out a confirmation code to your external email address.
Enter the code we sent you, or click the link in the message.
This will delete the account as well as all domains, users and aliases associated with this account.
This operation is irreversible.
As you will not be able to recover anything after this point, please make sure
that you have migrated all data before proceeding.
-
As we always strive to improve, we would like to ask for 2 minutes of your time.
+
+ As we always strive to improve, we would like to ask for 2 minutes of your time.
The best tool for improvement is feedback from users, and we would like to ask
for a few words about your reasons for leaving our service. Please send your feedback
- to support@kolabnow.com.
-
Also feel free to contact Kolab Now Support at support@kolabnow.com with any questions
+ to {{ supportEmail }}.
+
+
Also feel free to contact {{ appName }} Support with any questions
or concerns that you may have in this context.