Setup wizard update

This commit is contained in:
carodej
2020-07-01 11:01:54 +02:00
parent aedc98cc8b
commit a98625876d
46 changed files with 2487 additions and 263 deletions

67
.env.dev Normal file
View File

@@ -0,0 +1,67 @@
APP_NAME=VueFileManager
APP_ENV=local
APP_KEY=base64:EYM98pyseC/frZhW30ifeJqpOP3UmmLj1fMahrDN3zw=
APP_DEBUG=true
APP_URL=http://localhost
APP_DEMO=false
LOG_CHANNEL=stack
SCOUT_DRIVER=tntsearch
FILESYSTEM_DRIVER=local
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=file-manager
DB_USERNAME=root
DB_PASSWORD=root
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MAIL_DRIVER=smtp
MAIL_HOST=smtp.websupport.sk
MAIL_PORT=25
MAIL_USERNAME=vuefilemanager@hi5ve.digital
MAIL_PASSWORD=Yl2d]kET>)
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="${MAIL_USERNAME}"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=AKIA3UOZZXPUGKQ3BLMZ
AWS_SECRET_ACCESS_KEY=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
AWS_DEFAULT_REGION=eu-central-1
AWS_BUCKET=vuefilemanager-frankfurt
DO_SPACES_KEY=FSUHFOF5DMZCEWXVNPTI
DO_SPACES_SECRET=2t87Ugz/JoIjT1dIdTjnnKQ4t74Yfe665KBjCRc0yDk
DO_SPACES_ENDPOINT=https://fra1.digitaloceanspaces.com
DO_SPACES_REGION=fra1
DO_SPACES_BUCKET=vuefilemanager
WASABI_KEY=
WASABI_SECRET=
WASABI_ENDPOINT=
WASABI_REGION=
WASABI_BUCKET=
BACKBLAZE_KEY=
BACKBLAZE_SECRET=
BACKBLAZE_ENDPOINT=
BACKBLAZE_REGION=
BACKBLAZE_BUCKET=
PASSPORT_CLIENT_ID=1
PASSPORT_CLIENT_SECRET=kZevoWiMjuoxlfO0N0Ezq2oo6ukvX27VPEEJlQUD
APP_DEPLOY_SECRET=5603148y60eew0q5fw46
APP_DEPLOY_BRANCH=dev
CASHIER_LOGGER=stack
CASHIER_CURRENCY=usd
STRIPE_KEY=pk_test_51GsACaCBETHMUxzVsYkeApHtqb85paMuye7G77PDDQ28kXqDJ5HTmqLi13aM6xee81OQK1fhkTZ7vmDiWLStU9160061Yb2MtL
STRIPE_SECRET=sk_test_51GsACaCBETHMUxzVviYCrv0CeZMyWAOfBPe4uH5rkKJcJxrXhIciWQTr7UB1sgw9geoJMkNDVSWBQW36tuAsVznd00zhNHXhok
STRIPE_WEBHOOK_SECRET=whsec_5aM5emy4U9AzPLFxOPyBSyI0QGyI1MZW

View File

@@ -1,6 +1,6 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_KEY=base64:sB1YuKsbWv7MdWugb9ZsYBqv2QZJ+QOuHZHEddOsUuo=
APP_DEBUG=true
APP_URL=http://localhost
@@ -45,10 +45,25 @@ DO_SPACES_ENDPOINT=
DO_SPACES_REGION=
DO_SPACES_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
WASABI_KEY=
WASABI_SECRET=
WASABI_ENDPOINT=
WASABI_REGION=
WASABI_BUCKET=
BACKBLAZE_KEY=
BACKBLAZE_SECRET=
BACKBLAZE_ENDPOINT=
BACKBLAZE_REGION=
BACKBLAZE_BUCKET=
PASSPORT_CLIENT_ID=
PASSPORT_CLIENT_SECRET=
APP_DEPLOY_SECRET=
APP_DEPLOY_SECRET=
CASHIER_LOGGER=stack
CASHIER_CURRENCY=
STRIPE_KEY=
STRIPE_SECRET=
STRIPE_WEBHOOK_SECRET=

View File

@@ -63,9 +63,6 @@ class SetupProductionEnvironment extends Command
public function migrateDatabase()
{
$this->call('migrate:fresh');
$this->call('db:seed', [
'--class' => 'PaymentGatewaysSeeder'
]);
}
/**

View File

@@ -171,8 +171,6 @@ class UserController extends Controller
{
// Store avatar
if ($request->hasFile('avatar')) {
// Update avatar
$avatar = store_avatar($request->file('avatar'), 'avatars');
}

View File

@@ -20,20 +20,22 @@ class AuthController extends Controller
* @param Request $request
* @return mixed
*/
public function check_account(CheckAccountRequest $request) {
public function check_account(CheckAccountRequest $request)
{
// Get User
$user = User::where('email', $request->input('email'))->select(['name', 'avatar'])->first();
// Return user info
if ($user) return [
'name' => $user->name,
'name' => $user->name,
'avatar' => $user->avatar,
];
// Abort with 404, user not found
return abort('404', __('vuefilemanager.user_not_fount'));
}
/**
* Login user
*
@@ -42,17 +44,16 @@ class AuthController extends Controller
*/
public function login(Request $request)
{
$response = Route::dispatch(self::make_request($request));
$response = Route::dispatch(self::make_login_request($request));
if ($response->isSuccessful()) {
$data = json_decode($response->content(), true);
return response('Login Successfull!', 200)->cookie('access_token', $data['access_token'], 43200);
} else {
return $response;
}
return $response;
}
/**
@@ -64,11 +65,11 @@ class AuthController extends Controller
public function register(Request $request)
{
// Check if account registration is enabled
if (! config('vuefilemanager.registration') ) abort(401);
if (!config('vuefilemanager.registration')) abort(401);
// Validate request
$request->validate([
'name' => ['required', 'string', 'max:255'],
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:6', 'confirmed'],
]);
@@ -81,21 +82,22 @@ class AuthController extends Controller
]);
// Create settings
// TODO: set default storage capacity
$settings = UserSettings::create([
'user_id' => $user->id
'user_id' => $user->id,
'storage_capacity' => 5,
]);
$response = Route::dispatch(self::make_request($request));
$response = Route::dispatch(self::make_login_request($request));
if ($response->isSuccessful()) {
$data = json_decode($response->content(), true);
return response('Register Successfull!', 200)->cookie('access_token', $data['access_token'], 43200);
} else {
return $response;
}
return $response;
}
/**
@@ -106,7 +108,7 @@ class AuthController extends Controller
public function logout()
{
// Demo preview
if (is_demo( Auth::id())) {
if (is_demo(Auth::id())) {
return response('Logout successfull', 204)
->cookie('access_token', '', -1);
}
@@ -118,18 +120,17 @@ class AuthController extends Controller
$token->delete();
});
return response('Logout successfull', 204)
return response('Logout successful', 204)
->cookie('access_token', '', -1);
}
/**
* Make request for get user token
* Make login request for get access token
*
* @param Request $request
* @param string $provider
* @return Request
*/
private static function make_request($request)
private static function make_login_request($request)
{
$request->request->add([
'grant_type' => 'password',

View File

@@ -0,0 +1,607 @@
<?php
namespace App\Http\Controllers\General;
use App\Http\Controllers\Controller;
use App\Http\Requests\SetupWizard\CreateAdminRequest;
use App\Http\Requests\SetupWizard\StoreAppSetupRequest;
use App\Http\Requests\SetupWizard\StoreDatabaseCredentialsRequest;
use App\Http\Requests\SetupWizard\StoreEnvironmentSetupRequest;
use App\Http\Requests\SetupWizard\StoreStripeBillingRequest;
use App\Http\Requests\SetupWizard\StoreStripeCredentialsRequest;
use App\Http\Requests\SetupWizard\StoreStripePlansRequest;
use App\Services\StripeService;
use App\Setting;
use App\User;
use App\UserSettings;
use Artisan;
use Cartalyst\Stripe\Exception\UnauthorizedException;
use Doctrine\DBAL\Driver\PDOException;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Stripe;
use Symfony\Component\HttpKernel\Exception\HttpException;
class SetupWizardController extends Controller
{
/**
* Inject Stripe Service
*/
public function __construct(StripeService $stripe)
{
$this->stripe = $stripe;
}
/**
* Verify Envato purchase code
*
* @param Request $request
* @return ResponseFactory|\Illuminate\Http\Response|mixed
*/
public function verify_purchase_code(Request $request)
{
// Author API token
$token = 'X3kPkRnIHqauwE7vle3Gvhx6PTY9bvLr';
// e3420e63-ce6f-4d04-9b3e-f7f5cc6af7c6
// Verify purchase code
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $token
])->get('https://api.envato.com/v3/market/author/sale?code=' . $request->purchaseCode);
if ($response->successful()) {
return $response['license'];
}
return response('Purchase code is invalid.', 400);
}
/**
* Set up database credentials
*
* @param StoreDatabaseCredentialsRequest $request
* @return ResponseFactory|\Illuminate\Http\Response
*/
public function setup_database(StoreDatabaseCredentialsRequest $request)
{
// Set temporary database connection
config(['database.connections.test.driver' => $request->connection]);
config(['database.connections.test.host' => $request->host]);
config(['database.connections.test.port' => $request->port]);
config(['database.connections.test.database' => $request->name]);
config(['database.connections.test.username' => $request->username]);
config(['database.connections.test.password' => $request->password]);
// Test database connection
try {
\DB::connection('test')->getPdo();
} catch (PDOException $e) {
throw new HttpException(500, $e->getMessage());
}
$database_credentials = collect([
[
'name' => 'DB_CONNECTION',
'value' => $request->connection,
],
[
'name' => 'DB_HOST',
'value' => $request->host,
],
[
'name' => 'DB_PORT',
'value' => $request->port,
],
[
'name' => 'DB_DATABASE',
'value' => $request->name,
],
[
'name' => 'DB_USERNAME',
'value' => $request->username,
],
[
'name' => 'DB_PASSWORD',
'value' => $request->password,
],
]);
// Store database credentials
$database_credentials->each(function ($col) {
$this->setEnvironmentValue($col['name'], $col['value']);
});
// Set up application
$this->set_up_application();
return response('Done', 200);
}
/**
* Migrate database and generate necessary things
*/
private function set_up_application()
{
// Clear Cache
Artisan::call('cache:clear');
Artisan::call('config:clear');
// Generate app key
Artisan::call('key:generate');
// Migrate database
Artisan::call('migrate:fresh');
// Create Passport Keys
Artisan::call('passport:keys', [
'--force' => true
]);
// Create Password grant client
Artisan::call('passport:client', [
'--password' => true,
'--name' => 'vuefilemanager',
]);
// Create Personal access client
Artisan::call('passport:client', [
'--personal' => true,
'--name' => 'shared',
]);
// Get generated client
$client = \DB::table('oauth_clients')->where('name', '=', 'vuefilemanager')->first();
// Set passport client to .env
$this->setEnvironmentValue('PASSPORT_CLIENT_ID', $client->id);
$this->setEnvironmentValue('PASSPORT_CLIENT_SECRET', $client->secret);
}
/**
* Store and test stripe credentials
*
* @param StoreStripeCredentialsRequest $request
* @return ResponseFactory|\Illuminate\Http\Response
*/
public function store_stripe_credentials(StoreStripeCredentialsRequest $request)
{
// Create stripe instance
$stripe = Stripe::make($request->secret, '2020-03-02');
// Try to get stripe account details
try {
$stripe->account()->details();
} catch (UnauthorizedException $e) {
throw new HttpException(401, $e->getMessage());
}
// Get options
$settings = collect([
[
'name' => 'stripe_currency',
'value' => $request->currency,
],
[
'name' => 'stripe_webhook_secret',
'value' => $request->webhookSecret,
],
[
'name' => 'stripe_secret_key',
'value' => $request->secret,
],
[
'name' => 'stripe_publishable_key',
'value' => $request->key,
],
]);
// Set stripe credentials to .env
$this->setEnvironmentValue('CASHIER_CURRENCY', $request->currency);
$this->setEnvironmentValue('STRIPE_KEY', $request->key);
$this->setEnvironmentValue('STRIPE_SECRET', $request->secret);
$this->setEnvironmentValue('STRIPE_WEBHOOK_SECRET', $request->webhookSecret);
// Store options
$settings->each(function ($col) {
Setting::updateOrCreate(['name' => $col['name']], $col);
});
return response('Done', 200);
}
/**
* Store Stripe billings
*
* @param StoreStripeBillingRequest $request
* @return ResponseFactory|\Illuminate\Http\Response
*/
public function store_stripe_billings(StoreStripeBillingRequest $request)
{
// Get options
$settings = collect([
[
'name' => 'billing_phone_number',
'value' => $request->billing_phone_number,
],
[
'name' => 'billing_postal_code',
'value' => $request->billing_postal_code,
],
[
'name' => 'billing_vat_number',
'value' => $request->billing_vat_number,
],
[
'name' => 'billing_address',
'value' => $request->billing_address,
],
[
'name' => 'billing_country',
'value' => $request->billing_country,
],
[
'name' => 'billing_state',
'value' => $request->billing_state,
],
[
'name' => 'billing_city',
'value' => $request->billing_city,
],
[
'name' => 'billing_name',
'value' => $request->billing_name,
],
]);
// Store options
$settings->each(function ($col) {
Setting::updateOrCreate(['name' => $col['name']], $col);
});
return response('Done', 200);
}
/**
* Create Stripe subscription plan
*
* @param StoreStripePlansRequest $request
*/
public function store_stripe_plans(StoreStripePlansRequest $request)
{
foreach ($request->input('plans') as $plan) {
$this->stripe->createPlan($plan);
}
}
/**
* Store environment setup
*
* @param StoreEnvironmentSetupRequest $request
* @return string
*/
public function store_environment_setup(StoreEnvironmentSetupRequest $request)
{
$storage_driver = $request->input('storage.driver');
if ($storage_driver === 'local') {
$storage = collect([
[
'name' => 'FILESYSTEM_DRIVER',
'value' => 'local',
],
]);
} else if ($storage_driver === 's3') {
$storage = collect([
[
'name' => 'FILESYSTEM_DRIVER',
'value' => $request->input('storage.driver'),
],
[
'name' => 'AWS_ACCESS_KEY_ID',
'value' => $request->input('storage.key'),
],
[
'name' => 'AWS_SECRET_ACCESS_KEY',
'value' => $request->input('storage.secret'),
],
[
'name' => 'AWS_DEFAULT_REGION',
'value' => $request->input('storage.region'),
],
[
'name' => 'AWS_BUCKET',
'value' => $request->input('storage.bucket'),
],
]);
} else if ($storage_driver === 'spaces') {
$storage = collect([
[
'name' => 'FILESYSTEM_DRIVER',
'value' => $request->input('storage.driver'),
],
[
'name' => 'DO_SPACES_KEY',
'value' => $request->input('storage.key'),
],
[
'name' => 'DO_SPACES_SECRET',
'value' => $request->input('storage.secret'),
],
[
'name' => 'DO_SPACES_ENDPOINT',
'value' => $request->input('storage.endpoint'),
],
[
'name' => 'DO_SPACES_REGION',
'value' => $request->input('storage.region'),
],
[
'name' => 'DO_SPACES_BUCKET',
'value' => $request->input('storage.bucket'),
],
]);
} else if ($storage_driver === 'wasabi') {
$storage = collect([
[
'name' => 'FILESYSTEM_DRIVER',
'value' => $request->input('storage.driver'),
],
[
'name' => 'WASABI_KEY',
'value' => $request->input('storage.key'),
],
[
'name' => 'WASABI_SECRET',
'value' => $request->input('storage.secret'),
],
[
'name' => 'WASABI_ENDPOINT',
'value' => $request->input('storage.endpoint'),
],
[
'name' => 'WASABI_REGION',
'value' => $request->input('storage.region'),
],
[
'name' => 'WASABI_BUCKET',
'value' => $request->input('storage.bucket'),
],
]);
} else if ($storage_driver === 'backblaze') {
$storage = collect([
[
'name' => 'FILESYSTEM_DRIVER',
'value' => $request->input('storage.driver'),
],
[
'name' => 'BACKBLAZE_KEY',
'value' => $request->input('storage.key'),
],
[
'name' => 'BACKBLAZE_SECRET',
'value' => $request->input('storage.secret'),
],
[
'name' => 'BACKBLAZE_ENDPOINT',
'value' => $request->input('storage.endpoint'),
],
[
'name' => 'BACKBLAZE_REGION',
'value' => $request->input('storage.region'),
],
[
'name' => 'BACKBLAZE_BUCKET',
'value' => $request->input('storage.bucket'),
],
]);
}
// Store storage driver options
$storage->each(function ($col) {
$this->setEnvironmentValue($col['name'], $col['value']);
});
// Get options
$mail = collect([
[
'name' => 'MAIL_DRIVER',
'value' => $request->input('mail.driver'),
],
[
'name' => 'MAIL_HOST',
'value' => $request->input('mail.host'),
],
[
'name' => 'MAIL_PORT',
'value' => $request->input('mail.port'),
],
[
'name' => 'MAIL_USERNAME',
'value' => $request->input('mail.username'),
],
[
'name' => 'MAIL_PASSWORD',
'value' => $request->input('mail.password'),
],
[
'name' => 'MAIL_ENCRYPTION',
'value' => $request->input('mail.encryption'),
],
]);
// Store mail options
$mail->each(function ($col) {
$this->setEnvironmentValue($col['name'], $col['value']);
});
return response('Done', 200);
}
/**
* Store app settings
* @param StoreAppSetupRequest $request
* @return ResponseFactory|\Illuminate\Http\Response
*/
public function store_app_settings(StoreAppSetupRequest $request)
{
// Store Logo
if ($request->hasFile('logo')) {
$logo = store_system_image($request->file('logo'), 'system');
}
// Store favicon
if ($request->hasFile('favicon')) {
$favicon = store_system_image($request->file('favicon'), 'system');
}
// Get options
$settings = collect([
[
'name' => 'app_title',
'value' => $request->title,
],
[
'name' => 'app_description',
'value' => $request->description,
],
[
'name' => 'app_logo',
'value' => $request->hasFile('logo') ? $logo : null,
],
[
'name' => 'app_favicon',
'value' => $request->hasFile('favicon') ? $favicon : null,
],
[
'name' => 'google_analytics',
'value' => $request->googleAnalytics,
],
[
'name' => 'contact_email',
'value' => $request->contactMail,
],
[
'name' => 'registration',
'value' => $request->userRegistration,
],
[
'name' => 'storage_limitation',
'value' => $request->storageLimitation,
],
[
'name' => 'storage_default',
'value' => $request->defaultStorage,
],
]);
// Store options
$settings->each(function ($col) {
Setting::updateOrCreate(['name' => $col['name']], $col);
});
return response('Done', 200);
}
/**
* Create and login admin account
*
* @param Request $request
* @return ResponseFactory|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
*/
public function create_admin_account(Request $request)
{
// Validate request
$request->validate([
'email' => 'required|string|email|unique:users',
'password' => 'required|string|min:6|confirmed',
'name' => 'required|string',
'avatar' => 'sometimes|file',
]);
// Store avatar
if ($request->hasFile('avatar')) {
$avatar = store_avatar($request->file('avatar'), 'avatars');
}
// Create user
$user = User::create([
'avatar' => $request->hasFile('avatar') ? $avatar : null,
'name' => $request->name,
'role' => 'admin',
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Create settings
// TODO: set default storage capacity
UserSettings::create([
'user_id' => $user->id,
'storage_capacity' => 1,
]);
// Retrieve access token
$response = Route::dispatch(self::make_login_request($request));
// Send access token to user if request is successful
if ($response->isSuccessful()) {
$data = json_decode($response->content(), true);
return response('Admin was created', 200)->cookie('access_token', $data['access_token'], 43200);
}
return $response;
}
/**
* Make login request for get access token
*
* @param Request $request
* @return Request
*/
private static function make_login_request($request)
{
$request->request->add([
'grant_type' => 'password',
'client_id' => config('services.passport.client_id'),
'client_secret' => config('services.passport.client_secret'),
'username' => $request->email,
'password' => $request->password,
'scope' => 'master',
]);
return Request::create(url('/oauth/token'), 'POST', $request->all());
}
/**
* Set environment value
*
* @param $key
* @param $value
*/
public function setEnvironmentValue($key, $value)
{
$env_path = app()->environmentFilePath();
$escaped = preg_quote('=' . env($key), '/');
file_put_contents($env_path, preg_replace(
"/^{$key}{$escaped}/m",
$key . '=' . $value,
file_get_contents($env_path)
));
}
}

View File

@@ -179,6 +179,28 @@ function store_avatar($image, $path)
return $path . '/' . $image_path;
}
/**
* Store system image
*
* @param $image
* @param $path
* @return string
*/
function store_system_image($image, $path)
{
// Get directory
$path = check_directory($path);
// Store avatar
$image_path = Str::random(8) . '-' . str_replace(' ', '', $image->getClientOriginalName());
// Store image to disk
Storage::putFileAs($path, $image, $image_path);
// Return path to image
return $path . '/' . $image_path;
}
/**
* Check if directory exist, if no, then create it
*

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests\SetupWizard;
use Illuminate\Foundation\Http\FormRequest;
class StoreAppSetupRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required|string',
'description' => 'required|string',
'logo' => 'sometimes|file',
'favicon' => 'sometimes|file',
'contactMail' => 'required|email',
'googleAnalytics' => 'required|string',
'defaultStorage' => 'required|digits_between:1,9',
'userRegistration' => 'required|boolean',
'storageLimitation' => 'required|boolean',
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\SetupWizard;
use Illuminate\Foundation\Http\FormRequest;
class StoreDatabaseCredentialsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'connection' => 'required|string',
'host' => 'required|string',
'port' => 'required|string',
'name' => 'required|string',
'username' => 'required|string',
'password' => 'required|string',
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests\SetupWizard;
use Illuminate\Foundation\Http\FormRequest;
class StoreEnvironmentSetupRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'storage' => 'required|array',
'storage.driver' => 'required|string',
'storage.key' => 'sometimes|nullable|string',
'storage.secret' => 'sometimes|nullable|string',
'storage.endpoint' => 'sometimes|nullable|string',
'storage.region' => 'sometimes|nullable|string',
'storage.bucket' => 'sometimes|nullable|string',
'mail' => 'required|array',
'mail.driver' => 'required|string',
'mail.host' => 'required|string',
'mail.port' => 'required|string',
'mail.username' => 'required|string',
'mail.password' => 'required|string',
'mail.encryption' => 'required|string',
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests\SetupWizard;
use Illuminate\Foundation\Http\FormRequest;
class StoreStripeBillingRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'billing_phone_number' => 'sometimes|nullable|string',
'billing_postal_code' => 'required|string',
'billing_vat_number' => 'required|string',
'billing_address' => 'required|string',
'billing_country' => 'required|string',
'billing_state' => 'required|string',
'billing_city' => 'required|string',
'billing_name' => 'required|string',
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\SetupWizard;
use Illuminate\Foundation\Http\FormRequest;
class StoreStripeCredentialsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'currency' => 'required|string',
'webhookSecret' => 'required|string',
'secret' => 'required|string',
'key' => 'required|string',
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\SetupWizard;
use Illuminate\Foundation\Http\FormRequest;
class StoreStripePlansRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'plans' => 'required|array',
'plans.*.type' => 'required|string',
'plans.*.attributes.name' => 'required|string',
'plans.*.attributes.price' => 'required|string',
'plans.*.attributes.description' => 'sometimes|nullable|string',
'plans.*.attributes.capacity' => 'required|digits_between:1,9',
];
}
}

View File

@@ -5,6 +5,9 @@ namespace App\Providers;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Console\ClientCommand;
use Laravel\Passport\Console\InstallCommand;
use Laravel\Passport\Console\KeysCommand;
class AppServiceProvider extends ServiceProvider
{
@@ -31,5 +34,12 @@ class AppServiceProvider extends ServiceProvider
// Set locale for carbon dates
setlocale(LC_TIME, $get_time_locale);
// Install passport commands
$this->commands([
InstallCommand::class,
ClientCommand::class,
KeysCommand::class,
]);
}
}

View File

@@ -4,6 +4,8 @@
namespace App\Services;
use App\User;
use Artisan;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Laravel\Cashier\Exceptions\IncompletePayment;
use Stripe;
@@ -16,9 +18,16 @@ class StripeService
*/
public function __construct()
{
dd(config('stripe.secret'));
$this->stripe = Stripe::make(env('STRIPE_SECRET'), '2020-03-02');
}
/**
* Get Stripe account details
*
* @return mixed
*/
public function getAccountDetails()
{
$account = $this->stripe->account()->details();
@@ -112,6 +121,8 @@ class StripeService
}
/**
* Update customer details
*
* @param $user
*/
public function updateCustomerDetails($user)
@@ -205,22 +216,38 @@ class StripeService
/**
* Create plan
*
* @param $request
* @param $data
* @return mixed
*/
public function createPlan($request)
public function createPlan($data)
{
if ($data instanceof Request) {
$plan = [
'name' => $data->input('attributes.name'),
'description' => $data->input('attributes.description'),
'price' => $data->input('attributes.price'),
'capacity' => $data->input('attributes.capacity'),
];
} else {
$plan = [
'name' => $data['attributes']['name'],
'description' => $data['attributes']['description'],
'price' => $data['attributes']['price'],
'capacity' => $data['attributes']['capacity'],
];
}
$product = $this->stripe->products()->create([
'name' => $request->input('attributes.name'),
'description' => $request->input('attributes.description'),
'name' => $plan['name'],
'description' => $plan['description'],
'metadata' => [
'capacity' => $request->input('attributes.capacity')
'capacity' => $plan['capacity']
]
]);
$plan = $this->stripe->plans()->create([
'id' => Str::slug($request->input('attributes.name')),
'amount' => $request->input('attributes.price'),
'id' => Str::slug($plan['name']),
'amount' => $plan['price'],
'currency' => 'USD',
'interval' => 'month',
'product' => $product['id'],

View File

@@ -164,6 +164,7 @@ return [
TeamTNT\Scout\TNTSearchScoutServiceProvider::class,
Intervention\Image\ImageServiceProvider::class,
Laravel\Passport\PassportServiceProvider::class,
/*
* Package Service Providers...

View File

@@ -73,6 +73,24 @@ return [
'bucket' => env('DO_SPACES_BUCKET'),
],
'wasabi' => [
'driver' => 's3',
'key' => env('WASABI_KEY'),
'secret' => env('WASABI_SECRET'),
'endpoint' => env('WASABI_ENDPOINT'),
'region' => env('WASABI_REGION'),
'bucket' => env('WASABI_BUCKET'),
],
'backblaze' => [
'driver' => 's3',
'key' => env('BACKBLAZE_KEY'),
'secret' => env('BACKBLAZE_SECRET'),
'endpoint' => env('BACKBLAZE_ENDPOINT'),
'region' => env('BACKBLAZE_REGION'),
'bucket' => env('BACKBLAZE_BUCKET'),
],
],
];

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOauthAuthCodesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('oauth_auth_codes', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->unsignedBigInteger('user_id')->index();
$table->unsignedBigInteger('client_id');
$table->text('scopes')->nullable();
$table->boolean('revoked');
$table->dateTime('expires_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('oauth_auth_codes');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOauthAccessTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('oauth_access_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->unsignedBigInteger('user_id')->nullable()->index();
$table->unsignedBigInteger('client_id');
$table->string('name')->nullable();
$table->text('scopes')->nullable();
$table->boolean('revoked');
$table->timestamps();
$table->dateTime('expires_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('oauth_access_tokens');
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOauthRefreshTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('oauth_refresh_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->string('access_token_id', 100);
$table->boolean('revoked');
$table->dateTime('expires_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('oauth_refresh_tokens');
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOauthClientsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('oauth_clients', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id')->nullable()->index();
$table->string('name');
$table->string('secret', 100)->nullable();
$table->text('redirect');
$table->boolean('personal_access_client');
$table->boolean('password_client');
$table->boolean('revoked');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('oauth_clients');
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOauthPersonalAccessClientsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('oauth_personal_access_clients', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('client_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('oauth_personal_access_clients');
}
}

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCustomerColumns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('stripe_id')->nullable()->index();
$table->string('card_brand')->nullable();
$table->string('card_last_four', 4)->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'stripe_id',
'card_brand',
'card_last_four',
'trial_ends_at',
]);
});
}
}

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSubscriptionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('subscriptions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->string('name');
$table->string('stripe_id');
$table->string('stripe_status');
$table->string('stripe_plan')->nullable();
$table->integer('quantity')->nullable();
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'stripe_status']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('subscriptions');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSubscriptionItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('subscription_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('subscription_id');
$table->string('stripe_id')->index();
$table->string('stripe_plan');
$table->integer('quantity');
$table->timestamps();
$table->unique(['subscription_id', 'stripe_plan']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('subscription_items');
}
}

View File

@@ -1,40 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePaymentGatewaysTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('payment_gateways', function (Blueprint $table) {
$table->bigIncrements('id');
$table->boolean('status')->default(0);
$table->boolean('sandbox')->default(0);
$table->text('name');
$table->text('slug');
$table->text('logo');
$table->text('client_id')->nullable();
$table->text('secret')->nullable();
$table->text('webhook')->nullable();
$table->bigInteger('payment_processed')->default(0);
$table->longText('optional')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('payment_gateways');
}
}

View File

@@ -1,21 +0,0 @@
<?php
use Illuminate\Database\Seeder;
class PaymentGatewaysSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Create Stripe default record
DB::table('payment_gateways')->insert([
'logo' => '/assets/images/stripe-logo-thumbnail.png',
'name' => 'Stripe',
'slug' => 'stripe',
]);
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="96px" height="40px" viewBox="0 0 96 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
<title>stripe-service</title>
<desc>Created with Sketch.</desc>
<g id="Setup-Wizard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="3.-Payment-Select" transform="translate(-504.000000, -360.000000)" fill="#FFFFFF" fill-rule="nonzero">
<g id="Payment-option" transform="translate(476.000000, 333.000000)">
<path d="M34.746625,42.6742143 C34.746625,41.6329466 35.5999375,41.2322857 37.0135,41.2322857 C39.04,41.2322857 41.5999375,41.8466073 43.626625,42.9413841 L43.626625,36.6661785 C41.413375,35.7850625 39.22675,35.4379107 37.0135,35.4379107 C31.6,35.4379107 28,38.2684465 28,42.9948932 C28,50.3645 38.13325,49.1897413 38.13325,52.3674289 C38.13325,53.595509 37.06675,53.9961699 35.5733125,53.9961699 C33.3600625,53.9961699 30.5333125,53.0882055 28.29325,51.8599377 L28.29325,58.2151252 C30.7733125,59.2832413 33.28,59.7370358 35.5733125,59.7370358 C41.1199375,59.7370358 44.9333125,56.9868574 44.9333125,52.2070895 C44.9066875,44.2498216 34.746625,45.6649018 34.746625,42.6742143 Z M52.77325,30.0975179 L46.266625,31.4859374 L46.24,52.8745447 C46.24,56.8267057 49.2000625,59.7370358 53.14675,59.7370358 C55.333375,59.7370358 56.9333125,59.3365627 57.81325,58.8559198 L57.81325,53.4353573 C56.960125,53.7825091 52.746625,55.0107769 52.746625,51.0588036 L52.746625,41.5794375 L57.81325,41.5794375 L57.81325,35.8918929 L52.746625,35.8918929 L52.77325,30.0975179 Z M66.10675,37.8677857 L65.68,35.8918929 L59.92,35.8918929 L59.92,59.2563929 L66.58675,59.2563929 L66.58675,43.422027 C68.159875,41.3659645 70.8266875,41.739777 71.653375,42.0334197 L71.653375,35.8918929 C70.8000625,35.5714017 67.6800625,34.9839285 66.10675,37.8677857 Z M73.2799375,35.8918929 L79.9733125,35.8918929 L79.9733125,59.2563929 L73.2799375,59.2563929 L73.2799375,35.8918929 L73.2799375,35.8918929 Z M73.2799375,33.8624911 L79.9733125,32.4205625 L79.9733125,27 L73.2799375,28.4152679 L73.2799375,33.8623033 L73.2799375,33.8624911 Z M93.8933125,35.4379107 C91.2799375,35.4379107 89.5999375,36.6661785 88.66675,37.5208216 L88.319875,35.8652322 L82.453375,35.8652322 L82.453375,67 L89.1199375,65.5849198 L89.14675,58.0281251 C90.10675,58.7224287 91.5199375,59.7103751 93.8666875,59.7103751 C98.6400625,59.7103751 102.986687,55.8652322 102.986687,47.4006609 C102.960062,39.6568661 98.56,35.4379107 93.8933125,35.4379107 L93.8933125,35.4379107 Z M92.293375,53.8358304 C90.7200625,53.8358304 89.7865,53.2752056 89.14675,52.580902 L89.1199375,42.6742143 C89.8133125,41.8999287 90.7733125,41.3659645 92.293375,41.3659645 C94.72,41.3659645 96.4,44.0894822 96.4,47.5874732 C96.4,51.165634 94.746625,53.8358304 92.293375,53.8358304 L92.293375,53.8358304 Z M124,47.667643 C124,40.8318125 120.69325,35.4379107 114.373375,35.4379107 C108.0265,35.4379107 104.1865,40.8320003 104.1865,47.6143217 C104.1865,55.6515715 108.720063,59.7103751 115.2265,59.7103751 C118.399938,59.7103751 120.799938,58.9894108 122.61325,57.9748038 L122.61325,52.6342233 C120.800125,53.5421877 118.72,54.1030003 116.08,54.1030003 C113.49325,54.1030003 111.199938,53.1950359 110.906688,50.0441966 L123.94675,50.0441966 C123.94675,49.696857 124,48.3084375 124,47.667643 Z M110.826625,45.1309376 C110.826625,42.1135894 112.66675,40.8584732 114.34675,40.8584732 C115.973313,40.8584732 117.70675,42.1135894 117.70675,45.1309376 L110.826625,45.1309376 Z" id="stripe-service"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -10,5 +10,276 @@
"/js/main.ec4459f3993385a8ffb0.hot-update.js": "/js/main.ec4459f3993385a8ffb0.hot-update.js",
"/js/main.4aa483cf186b82e6f133.hot-update.js": "/js/main.4aa483cf186b82e6f133.hot-update.js",
"/js/main.0b62890fe58f6191cff2.hot-update.js": "/js/main.0b62890fe58f6191cff2.hot-update.js",
"/js/main.8f85a1128dbc59267b4d.hot-update.js": "/js/main.8f85a1128dbc59267b4d.hot-update.js"
"/js/main.8f85a1128dbc59267b4d.hot-update.js": "/js/main.8f85a1128dbc59267b4d.hot-update.js",
"/js/main.f57c08e4f6453554eef7.hot-update.js": "/js/main.f57c08e4f6453554eef7.hot-update.js",
"/js/main.49806237173f17d9145e.hot-update.js": "/js/main.49806237173f17d9145e.hot-update.js",
"/js/main.a66ef529551f3908fa83.hot-update.js": "/js/main.a66ef529551f3908fa83.hot-update.js",
"/js/main.d250bde2dfd0cb83ead1.hot-update.js": "/js/main.d250bde2dfd0cb83ead1.hot-update.js",
"/js/main.86c738013294c543bf9f.hot-update.js": "/js/main.86c738013294c543bf9f.hot-update.js",
"/js/main.7d5ddf17e4f5ac5d7774.hot-update.js": "/js/main.7d5ddf17e4f5ac5d7774.hot-update.js",
"/js/main.ac3441b3dd689cf2a474.hot-update.js": "/js/main.ac3441b3dd689cf2a474.hot-update.js",
"/js/main.c17831922217a8e0ca69.hot-update.js": "/js/main.c17831922217a8e0ca69.hot-update.js",
"/js/main.047d29a9f0934470628c.hot-update.js": "/js/main.047d29a9f0934470628c.hot-update.js",
"/js/main.a12c2cbb9d4f587cb915.hot-update.js": "/js/main.a12c2cbb9d4f587cb915.hot-update.js",
"/js/main.eb91f9aef7a46fc4d97d.hot-update.js": "/js/main.eb91f9aef7a46fc4d97d.hot-update.js",
"/js/main.8950f2c1f965b34c67c8.hot-update.js": "/js/main.8950f2c1f965b34c67c8.hot-update.js",
"/js/main.c181b6f235365cc77806.hot-update.js": "/js/main.c181b6f235365cc77806.hot-update.js",
"/js/main.432d249065d06227b881.hot-update.js": "/js/main.432d249065d06227b881.hot-update.js",
"/js/main.6ab44d447ea09576bf79.hot-update.js": "/js/main.6ab44d447ea09576bf79.hot-update.js",
"/js/main.f877b4f51fa54d34a703.hot-update.js": "/js/main.f877b4f51fa54d34a703.hot-update.js",
"/js/main.bfb7a5ed9a7a787bf1b1.hot-update.js": "/js/main.bfb7a5ed9a7a787bf1b1.hot-update.js",
"/js/main.7f79a324bd9f3e34330e.hot-update.js": "/js/main.7f79a324bd9f3e34330e.hot-update.js",
"/js/main.7ab0530f4fa616fd6479.hot-update.js": "/js/main.7ab0530f4fa616fd6479.hot-update.js",
"/js/main.10376c1cb6a085c800d6.hot-update.js": "/js/main.10376c1cb6a085c800d6.hot-update.js",
"/js/main.c5c07ed33eaae7e74bb0.hot-update.js": "/js/main.c5c07ed33eaae7e74bb0.hot-update.js",
"/js/main.b3898569822a4bea728a.hot-update.js": "/js/main.b3898569822a4bea728a.hot-update.js",
"/js/main.cb3e97b7379bb2690e80.hot-update.js": "/js/main.cb3e97b7379bb2690e80.hot-update.js",
"/js/main.3907d0c494c707a2e526.hot-update.js": "/js/main.3907d0c494c707a2e526.hot-update.js",
"/js/main.30c46b47c913c6750487.hot-update.js": "/js/main.30c46b47c913c6750487.hot-update.js",
"/js/main.63ac156900e0c0a6a163.hot-update.js": "/js/main.63ac156900e0c0a6a163.hot-update.js",
"/js/main.b0b834bae1baf11b7d2e.hot-update.js": "/js/main.b0b834bae1baf11b7d2e.hot-update.js",
"/js/main.ddb0da2f7522ea677587.hot-update.js": "/js/main.ddb0da2f7522ea677587.hot-update.js",
"/js/main.6b767b34769556b6f22e.hot-update.js": "/js/main.6b767b34769556b6f22e.hot-update.js",
"/js/main.388c60436fa8234d3415.hot-update.js": "/js/main.388c60436fa8234d3415.hot-update.js",
"/js/main.3c144c24f3ae3943bc90.hot-update.js": "/js/main.3c144c24f3ae3943bc90.hot-update.js",
"/js/main.bf2d7c2fb972e7d888cc.hot-update.js": "/js/main.bf2d7c2fb972e7d888cc.hot-update.js",
"/js/main.a1260c2810a8264c42e2.hot-update.js": "/js/main.a1260c2810a8264c42e2.hot-update.js",
"/js/main.d4e8446c38a1548413bb.hot-update.js": "/js/main.d4e8446c38a1548413bb.hot-update.js",
"/js/main.129032d6ff4ea189d691.hot-update.js": "/js/main.129032d6ff4ea189d691.hot-update.js",
"/js/main.bdff6b7e32d4e63268a9.hot-update.js": "/js/main.bdff6b7e32d4e63268a9.hot-update.js",
"/js/main.bb35fbbacbb8cc6a21be.hot-update.js": "/js/main.bb35fbbacbb8cc6a21be.hot-update.js",
"/js/main.f49adc3a4f0766e49fb8.hot-update.js": "/js/main.f49adc3a4f0766e49fb8.hot-update.js",
"/js/main.57e0a8a6d1694937d714.hot-update.js": "/js/main.57e0a8a6d1694937d714.hot-update.js",
"/js/main.62cbda0a1a4c8c1e7ddb.hot-update.js": "/js/main.62cbda0a1a4c8c1e7ddb.hot-update.js",
"/js/main.bfe07da70f88cbc00a18.hot-update.js": "/js/main.bfe07da70f88cbc00a18.hot-update.js",
"/js/main.1fc88be6d6fc7818d38c.hot-update.js": "/js/main.1fc88be6d6fc7818d38c.hot-update.js",
"/js/main.6575481131214e9c2891.hot-update.js": "/js/main.6575481131214e9c2891.hot-update.js",
"/js/main.c99aec40c2c3ee3eac0d.hot-update.js": "/js/main.c99aec40c2c3ee3eac0d.hot-update.js",
"/js/main.e525917a75cbb76c8b26.hot-update.js": "/js/main.e525917a75cbb76c8b26.hot-update.js",
"/js/main.22c20ab0c219852049ad.hot-update.js": "/js/main.22c20ab0c219852049ad.hot-update.js",
"/js/main.df5b1efd5e4b9c9bac80.hot-update.js": "/js/main.df5b1efd5e4b9c9bac80.hot-update.js",
"/js/main.26a5f721dbce5fad22f7.hot-update.js": "/js/main.26a5f721dbce5fad22f7.hot-update.js",
"/js/main.0d520a66a47c10f5f5e2.hot-update.js": "/js/main.0d520a66a47c10f5f5e2.hot-update.js",
"/js/main.e3d18eca82e2431524f8.hot-update.js": "/js/main.e3d18eca82e2431524f8.hot-update.js",
"/js/main.3f7d42db34a8638638f5.hot-update.js": "/js/main.3f7d42db34a8638638f5.hot-update.js",
"/js/main.12b18448bcb03190696e.hot-update.js": "/js/main.12b18448bcb03190696e.hot-update.js",
"/js/main.f1bdbd6d4e1b6125a751.hot-update.js": "/js/main.f1bdbd6d4e1b6125a751.hot-update.js",
"/js/main.7f954acdffdef3ba1b4e.hot-update.js": "/js/main.7f954acdffdef3ba1b4e.hot-update.js",
"/js/main.8af19bb7c91195200c83.hot-update.js": "/js/main.8af19bb7c91195200c83.hot-update.js",
"/js/main.83950ac988f7d52fd4db.hot-update.js": "/js/main.83950ac988f7d52fd4db.hot-update.js",
"/js/main.2668f11eaec9054d4f85.hot-update.js": "/js/main.2668f11eaec9054d4f85.hot-update.js",
"/js/main.0cb7453215e2457e963d.hot-update.js": "/js/main.0cb7453215e2457e963d.hot-update.js",
"/js/main.628d6b45839c0bd50ffa.hot-update.js": "/js/main.628d6b45839c0bd50ffa.hot-update.js",
"/js/main.494744531adf85766dd7.hot-update.js": "/js/main.494744531adf85766dd7.hot-update.js",
"/js/main.e234521251663454e5bb.hot-update.js": "/js/main.e234521251663454e5bb.hot-update.js",
"/js/main.0f8aceea8e819824f7f4.hot-update.js": "/js/main.0f8aceea8e819824f7f4.hot-update.js",
"/js/main.1c4b31ce03bd281c7af0.hot-update.js": "/js/main.1c4b31ce03bd281c7af0.hot-update.js",
"/js/main.23395410004c25e9e752.hot-update.js": "/js/main.23395410004c25e9e752.hot-update.js",
"/js/main.dec2f01b8b3413b07cce.hot-update.js": "/js/main.dec2f01b8b3413b07cce.hot-update.js",
"/js/main.90d5738c7eca52e99fa1.hot-update.js": "/js/main.90d5738c7eca52e99fa1.hot-update.js",
"/js/main.350c381ae5ef35075008.hot-update.js": "/js/main.350c381ae5ef35075008.hot-update.js",
"/js/main.7ff8cd4e0f8129b28020.hot-update.js": "/js/main.7ff8cd4e0f8129b28020.hot-update.js",
"/js/main.36981f38ef6dbe1ad2ef.hot-update.js": "/js/main.36981f38ef6dbe1ad2ef.hot-update.js",
"/js/main.c72e92249cae6c72de71.hot-update.js": "/js/main.c72e92249cae6c72de71.hot-update.js",
"/js/main.8d4d5d88816dfa6a6c3a.hot-update.js": "/js/main.8d4d5d88816dfa6a6c3a.hot-update.js",
"/js/main.6315104b66153dff7594.hot-update.js": "/js/main.6315104b66153dff7594.hot-update.js",
"/js/main.4c5a2605aac797ae5094.hot-update.js": "/js/main.4c5a2605aac797ae5094.hot-update.js",
"/js/main.06385ccc6884270b68f0.hot-update.js": "/js/main.06385ccc6884270b68f0.hot-update.js",
"/js/main.b515044cb3386d6eb38c.hot-update.js": "/js/main.b515044cb3386d6eb38c.hot-update.js",
"/js/main.daf3544f7ebd524ba8b2.hot-update.js": "/js/main.daf3544f7ebd524ba8b2.hot-update.js",
"/js/main.f49dced487020b2401db.hot-update.js": "/js/main.f49dced487020b2401db.hot-update.js",
"/js/main.61e51b120958c98e9b80.hot-update.js": "/js/main.61e51b120958c98e9b80.hot-update.js",
"/js/main.4428c57138139124d638.hot-update.js": "/js/main.4428c57138139124d638.hot-update.js",
"/js/main.2b05f7597b38aa32f3d3.hot-update.js": "/js/main.2b05f7597b38aa32f3d3.hot-update.js",
"/js/main.b6055028d561f898e936.hot-update.js": "/js/main.b6055028d561f898e936.hot-update.js",
"/js/main.78472a4d15e86cf01c4c.hot-update.js": "/js/main.78472a4d15e86cf01c4c.hot-update.js",
"/js/main.210d791d65e133956107.hot-update.js": "/js/main.210d791d65e133956107.hot-update.js",
"/js/main.ffa9b0ac8442e4d2f24b.hot-update.js": "/js/main.ffa9b0ac8442e4d2f24b.hot-update.js",
"/js/main.e2a830fd08baece2fac6.hot-update.js": "/js/main.e2a830fd08baece2fac6.hot-update.js",
"/js/main.d389a917baec2038604b.hot-update.js": "/js/main.d389a917baec2038604b.hot-update.js",
"/js/main.3e0bc14c6d62109be8c1.hot-update.js": "/js/main.3e0bc14c6d62109be8c1.hot-update.js",
"/js/main.f950652035d59e80ba88.hot-update.js": "/js/main.f950652035d59e80ba88.hot-update.js",
"/js/main.f43b68d5594c47aa9da3.hot-update.js": "/js/main.f43b68d5594c47aa9da3.hot-update.js",
"/js/main.e7caa0957bb1286588d4.hot-update.js": "/js/main.e7caa0957bb1286588d4.hot-update.js",
"/js/main.95d769d0027eed99e6c1.hot-update.js": "/js/main.95d769d0027eed99e6c1.hot-update.js",
"/js/main.77762b16026f7742cbfa.hot-update.js": "/js/main.77762b16026f7742cbfa.hot-update.js",
"/js/main.dec59254037d7f896b86.hot-update.js": "/js/main.dec59254037d7f896b86.hot-update.js",
"/js/main.a1cc927c6a50ee0c7c61.hot-update.js": "/js/main.a1cc927c6a50ee0c7c61.hot-update.js",
"/js/main.bac3134e1b5eb0e23fea.hot-update.js": "/js/main.bac3134e1b5eb0e23fea.hot-update.js",
"/js/main.d42c1993753ba7c6d7bc.hot-update.js": "/js/main.d42c1993753ba7c6d7bc.hot-update.js",
"/js/main.fe48b2784aca36a6dfbc.hot-update.js": "/js/main.fe48b2784aca36a6dfbc.hot-update.js",
"/js/main.f952b84184c741f68c44.hot-update.js": "/js/main.f952b84184c741f68c44.hot-update.js",
"/js/main.22f72f1e3bcce35c118d.hot-update.js": "/js/main.22f72f1e3bcce35c118d.hot-update.js",
"/js/main.294962aafc6753ce4207.hot-update.js": "/js/main.294962aafc6753ce4207.hot-update.js",
"/js/main.d36f9a9f6222aa74bc38.hot-update.js": "/js/main.d36f9a9f6222aa74bc38.hot-update.js",
"/js/main.0118b88fb836f75f907c.hot-update.js": "/js/main.0118b88fb836f75f907c.hot-update.js",
"/js/main.a615663568a78691f42f.hot-update.js": "/js/main.a615663568a78691f42f.hot-update.js",
"/js/main.8881ab5062a0b552e57a.hot-update.js": "/js/main.8881ab5062a0b552e57a.hot-update.js",
"/js/main.e49de8f4cfa70d1c4935.hot-update.js": "/js/main.e49de8f4cfa70d1c4935.hot-update.js",
"/js/main.3dfcd8d63417db8974ce.hot-update.js": "/js/main.3dfcd8d63417db8974ce.hot-update.js",
"/js/main.a9ce2ef2a19666a6479c.hot-update.js": "/js/main.a9ce2ef2a19666a6479c.hot-update.js",
"/js/main.2c12c92e2c72d1e41fd0.hot-update.js": "/js/main.2c12c92e2c72d1e41fd0.hot-update.js",
"/js/main.49f0ffdafe50bb99c8d4.hot-update.js": "/js/main.49f0ffdafe50bb99c8d4.hot-update.js",
"/js/main.fa9016e6dd018e588900.hot-update.js": "/js/main.fa9016e6dd018e588900.hot-update.js",
"/js/main.2b915d9e98818d0176a6.hot-update.js": "/js/main.2b915d9e98818d0176a6.hot-update.js",
"/js/main.e87a7bb1c041232220b3.hot-update.js": "/js/main.e87a7bb1c041232220b3.hot-update.js",
"/js/main.cd116661c4a1d6c5d69c.hot-update.js": "/js/main.cd116661c4a1d6c5d69c.hot-update.js",
"/js/main.582a1d128d577aec65b7.hot-update.js": "/js/main.582a1d128d577aec65b7.hot-update.js",
"/js/main.571bd2027b0ad0ac5962.hot-update.js": "/js/main.571bd2027b0ad0ac5962.hot-update.js",
"/js/main.5e91c2e331ccdfd30443.hot-update.js": "/js/main.5e91c2e331ccdfd30443.hot-update.js",
"/js/main.e7e13a6e09678c73939d.hot-update.js": "/js/main.e7e13a6e09678c73939d.hot-update.js",
"/js/main.5116d49d4cfe29bd317c.hot-update.js": "/js/main.5116d49d4cfe29bd317c.hot-update.js",
"/js/main.18894bdbb14cb9e2f4e6.hot-update.js": "/js/main.18894bdbb14cb9e2f4e6.hot-update.js",
"/js/main.4d7deb55af6c2e02cb9f.hot-update.js": "/js/main.4d7deb55af6c2e02cb9f.hot-update.js",
"/js/main.59c52d7a2f19a0cecb7c.hot-update.js": "/js/main.59c52d7a2f19a0cecb7c.hot-update.js",
"/js/main.78c184149bfbc5ca6932.hot-update.js": "/js/main.78c184149bfbc5ca6932.hot-update.js",
"/js/main.51cbbf7dc86f2a2012fa.hot-update.js": "/js/main.51cbbf7dc86f2a2012fa.hot-update.js",
"/js/main.d012d1169cb534877cd0.hot-update.js": "/js/main.d012d1169cb534877cd0.hot-update.js",
"/js/main.994c6d9f812ba73875da.hot-update.js": "/js/main.994c6d9f812ba73875da.hot-update.js",
"/js/main.fb808d6466d2dd3de777.hot-update.js": "/js/main.fb808d6466d2dd3de777.hot-update.js",
"/js/main.96d27c5eba7218a1953c.hot-update.js": "/js/main.96d27c5eba7218a1953c.hot-update.js",
"/js/main.222ebcd0c3e9d30a10a7.hot-update.js": "/js/main.222ebcd0c3e9d30a10a7.hot-update.js",
"/js/main.ec324946af056d4e9e40.hot-update.js": "/js/main.ec324946af056d4e9e40.hot-update.js",
"/js/main.cf59f620f34b75f33d57.hot-update.js": "/js/main.cf59f620f34b75f33d57.hot-update.js",
"/js/main.40983dcc9efdc97efb63.hot-update.js": "/js/main.40983dcc9efdc97efb63.hot-update.js",
"/js/main.c047afe25c97538cd057.hot-update.js": "/js/main.c047afe25c97538cd057.hot-update.js",
"/js/main.22af859fdba4f4884580.hot-update.js": "/js/main.22af859fdba4f4884580.hot-update.js",
"/js/main.2977f19ca914fd5a8b6a.hot-update.js": "/js/main.2977f19ca914fd5a8b6a.hot-update.js",
"/js/main.a5e6c89e84d4ad81c052.hot-update.js": "/js/main.a5e6c89e84d4ad81c052.hot-update.js",
"/js/main.42b8fe8b9a648085bd8e.hot-update.js": "/js/main.42b8fe8b9a648085bd8e.hot-update.js",
"/js/main.f0cfa4545280f946ba4a.hot-update.js": "/js/main.f0cfa4545280f946ba4a.hot-update.js",
"/js/main.85e3c644ebd07ea49ec3.hot-update.js": "/js/main.85e3c644ebd07ea49ec3.hot-update.js",
"/js/main.ece75643d67e9bcbe226.hot-update.js": "/js/main.ece75643d67e9bcbe226.hot-update.js",
"/js/main.6a23a146600381ea9131.hot-update.js": "/js/main.6a23a146600381ea9131.hot-update.js",
"/js/main.bc3bf0a6e8cc685e89a1.hot-update.js": "/js/main.bc3bf0a6e8cc685e89a1.hot-update.js",
"/js/main.afece1ee9777a0f09f92.hot-update.js": "/js/main.afece1ee9777a0f09f92.hot-update.js",
"/js/main.318eeefc5c617a59ee6d.hot-update.js": "/js/main.318eeefc5c617a59ee6d.hot-update.js",
"/js/main.755f6694f38c8c77bd27.hot-update.js": "/js/main.755f6694f38c8c77bd27.hot-update.js",
"/js/main.f18d478e890997d4f4da.hot-update.js": "/js/main.f18d478e890997d4f4da.hot-update.js",
"/js/main.db0f3a4f426d3990e240.hot-update.js": "/js/main.db0f3a4f426d3990e240.hot-update.js",
"/js/main.a405076120f1f39d6d6d.hot-update.js": "/js/main.a405076120f1f39d6d6d.hot-update.js",
"/js/main.6cba9147fd829edb958f.hot-update.js": "/js/main.6cba9147fd829edb958f.hot-update.js",
"/js/main.cd2fe4e3030ca6acc4e1.hot-update.js": "/js/main.cd2fe4e3030ca6acc4e1.hot-update.js",
"/js/main.5cded47a7b477704e2eb.hot-update.js": "/js/main.5cded47a7b477704e2eb.hot-update.js",
"/js/main.c7486336a6790360832d.hot-update.js": "/js/main.c7486336a6790360832d.hot-update.js",
"/js/main.f7016f686c1057bc13e3.hot-update.js": "/js/main.f7016f686c1057bc13e3.hot-update.js",
"/js/main.17c68ee60b815575d2f9.hot-update.js": "/js/main.17c68ee60b815575d2f9.hot-update.js",
"/js/main.86a4ab5670f7d5cabb35.hot-update.js": "/js/main.86a4ab5670f7d5cabb35.hot-update.js",
"/js/main.4dea91e54b0a38bba6e7.hot-update.js": "/js/main.4dea91e54b0a38bba6e7.hot-update.js",
"/js/main.feb31c4f536800f14886.hot-update.js": "/js/main.feb31c4f536800f14886.hot-update.js",
"/js/main.c47820cef3354299e493.hot-update.js": "/js/main.c47820cef3354299e493.hot-update.js",
"/js/main.5a3111773041d077914c.hot-update.js": "/js/main.5a3111773041d077914c.hot-update.js",
"/js/main.6c1dc0ab6b48b7fb8d0f.hot-update.js": "/js/main.6c1dc0ab6b48b7fb8d0f.hot-update.js",
"/js/main.acb7922f38d63ffedb8c.hot-update.js": "/js/main.acb7922f38d63ffedb8c.hot-update.js",
"/js/main.a53e0c7d11808569d268.hot-update.js": "/js/main.a53e0c7d11808569d268.hot-update.js",
"/js/main.a46a827c478e8ab2393e.hot-update.js": "/js/main.a46a827c478e8ab2393e.hot-update.js",
"/js/main.701befe377412289c1e7.hot-update.js": "/js/main.701befe377412289c1e7.hot-update.js",
"/js/main.4b1e0972a8490afd2480.hot-update.js": "/js/main.4b1e0972a8490afd2480.hot-update.js",
"/js/main.a12d0df8441fe0b1b64e.hot-update.js": "/js/main.a12d0df8441fe0b1b64e.hot-update.js",
"/js/main.8a13cdc450f54124a2e8.hot-update.js": "/js/main.8a13cdc450f54124a2e8.hot-update.js",
"/js/main.86ad86b8ab0ac84651d7.hot-update.js": "/js/main.86ad86b8ab0ac84651d7.hot-update.js",
"/js/main.d4311ae5421503ee73ba.hot-update.js": "/js/main.d4311ae5421503ee73ba.hot-update.js",
"/js/main.a12f2b869039233ec7d1.hot-update.js": "/js/main.a12f2b869039233ec7d1.hot-update.js",
"/js/main.4331e216c81ad63d68d2.hot-update.js": "/js/main.4331e216c81ad63d68d2.hot-update.js",
"/js/main.a9f4434e2ef7b9e029f9.hot-update.js": "/js/main.a9f4434e2ef7b9e029f9.hot-update.js",
"/js/main.f4cf1d836ab8fad25b18.hot-update.js": "/js/main.f4cf1d836ab8fad25b18.hot-update.js",
"/js/main.714ab8accd45fca0352c.hot-update.js": "/js/main.714ab8accd45fca0352c.hot-update.js",
"/js/main.f4eb0b9755cffa4dfcc7.hot-update.js": "/js/main.f4eb0b9755cffa4dfcc7.hot-update.js",
"/js/main.e970c4c1f36844c2977f.hot-update.js": "/js/main.e970c4c1f36844c2977f.hot-update.js",
"/js/main.78570685fbf7d85246f2.hot-update.js": "/js/main.78570685fbf7d85246f2.hot-update.js",
"/js/main.fb03325bcaa8d4d98090.hot-update.js": "/js/main.fb03325bcaa8d4d98090.hot-update.js",
"/js/main.4d26215591306c839618.hot-update.js": "/js/main.4d26215591306c839618.hot-update.js",
"/js/main.2d1f539b131e5873a485.hot-update.js": "/js/main.2d1f539b131e5873a485.hot-update.js",
"/js/main.835a770181065c1b9397.hot-update.js": "/js/main.835a770181065c1b9397.hot-update.js",
"/js/main.09f9dab42bcfa1634f4d.hot-update.js": "/js/main.09f9dab42bcfa1634f4d.hot-update.js",
"/js/main.8a70e542c3e3304535a4.hot-update.js": "/js/main.8a70e542c3e3304535a4.hot-update.js",
"/js/main.325adf8cf5e1a83126a5.hot-update.js": "/js/main.325adf8cf5e1a83126a5.hot-update.js",
"/js/main.f961b6e9d077a442035a.hot-update.js": "/js/main.f961b6e9d077a442035a.hot-update.js",
"/js/main.60d318b86ef1942116fc.hot-update.js": "/js/main.60d318b86ef1942116fc.hot-update.js",
"/js/main.fa7aca50202d520eff5c.hot-update.js": "/js/main.fa7aca50202d520eff5c.hot-update.js",
"/js/main.c61bd9bd5f3bd0f7bfaf.hot-update.js": "/js/main.c61bd9bd5f3bd0f7bfaf.hot-update.js",
"/js/main.ebf01927b1c1132074bb.hot-update.js": "/js/main.ebf01927b1c1132074bb.hot-update.js",
"/js/main.73ceb4ddc97dd83e3e9d.hot-update.js": "/js/main.73ceb4ddc97dd83e3e9d.hot-update.js",
"/js/main.d59fcd1a37bfee22bf4f.hot-update.js": "/js/main.d59fcd1a37bfee22bf4f.hot-update.js",
"/js/main.94b332e4c7a0031ed8ef.hot-update.js": "/js/main.94b332e4c7a0031ed8ef.hot-update.js",
"/js/main.578348615f140107a146.hot-update.js": "/js/main.578348615f140107a146.hot-update.js",
"/js/main.4d0d19eb2776ac141cf3.hot-update.js": "/js/main.4d0d19eb2776ac141cf3.hot-update.js",
"/js/main.c6c7699155e707c83638.hot-update.js": "/js/main.c6c7699155e707c83638.hot-update.js",
"/js/main.cc2b80170d644f9fb024.hot-update.js": "/js/main.cc2b80170d644f9fb024.hot-update.js",
"/js/main.7d835be6842b8c9b22ac.hot-update.js": "/js/main.7d835be6842b8c9b22ac.hot-update.js",
"/js/main.c48c478a7083284384b4.hot-update.js": "/js/main.c48c478a7083284384b4.hot-update.js",
"/js/main.0a033e7788ae142bef26.hot-update.js": "/js/main.0a033e7788ae142bef26.hot-update.js",
"/js/main.0b7dfc2f94ca7c20da89.hot-update.js": "/js/main.0b7dfc2f94ca7c20da89.hot-update.js",
"/js/main.321b6748d866ee25e6a0.hot-update.js": "/js/main.321b6748d866ee25e6a0.hot-update.js",
"/js/main.0fc25b4b50854ab03ecb.hot-update.js": "/js/main.0fc25b4b50854ab03ecb.hot-update.js",
"/js/main.d4eb7381630781b7f83b.hot-update.js": "/js/main.d4eb7381630781b7f83b.hot-update.js",
"/js/main.dbf7b9a6b03dd3e764f5.hot-update.js": "/js/main.dbf7b9a6b03dd3e764f5.hot-update.js",
"/js/main.f2d175ee3ad7394744f3.hot-update.js": "/js/main.f2d175ee3ad7394744f3.hot-update.js",
"/js/main.7097f18bafbb2c782e34.hot-update.js": "/js/main.7097f18bafbb2c782e34.hot-update.js",
"/js/main.e74278efede5c77fb59a.hot-update.js": "/js/main.e74278efede5c77fb59a.hot-update.js",
"/js/main.b16252a3f321aa5f638c.hot-update.js": "/js/main.b16252a3f321aa5f638c.hot-update.js",
"/js/main.65d67c907472cea149a2.hot-update.js": "/js/main.65d67c907472cea149a2.hot-update.js",
"/js/main.d32086da1adce30a365c.hot-update.js": "/js/main.d32086da1adce30a365c.hot-update.js",
"/js/main.2abe2dd3690d4223aae6.hot-update.js": "/js/main.2abe2dd3690d4223aae6.hot-update.js",
"/js/main.34d101ff4e9c0f3c9f62.hot-update.js": "/js/main.34d101ff4e9c0f3c9f62.hot-update.js",
"/js/main.db8a2c676ae81a86ac07.hot-update.js": "/js/main.db8a2c676ae81a86ac07.hot-update.js",
"/js/main.3e10515c75b547edbd84.hot-update.js": "/js/main.3e10515c75b547edbd84.hot-update.js",
"/js/main.a21b2c4011d251b159dc.hot-update.js": "/js/main.a21b2c4011d251b159dc.hot-update.js",
"/js/main.29892a9f1673876f812a.hot-update.js": "/js/main.29892a9f1673876f812a.hot-update.js",
"/js/main.90e4e646ca4687f65d71.hot-update.js": "/js/main.90e4e646ca4687f65d71.hot-update.js",
"/js/main.2f329e24c50c195b7655.hot-update.js": "/js/main.2f329e24c50c195b7655.hot-update.js",
"/js/main.a8a8ecc1aa6706cf3eb7.hot-update.js": "/js/main.a8a8ecc1aa6706cf3eb7.hot-update.js",
"/js/main.e03c44a34c09251882cb.hot-update.js": "/js/main.e03c44a34c09251882cb.hot-update.js",
"/js/main.1c1d02f7cd7ffae319bd.hot-update.js": "/js/main.1c1d02f7cd7ffae319bd.hot-update.js",
"/js/main.a1fed26b6036e6849112.hot-update.js": "/js/main.a1fed26b6036e6849112.hot-update.js",
"/js/main.c4018662de1655065788.hot-update.js": "/js/main.c4018662de1655065788.hot-update.js",
"/js/main.330cb26c0bc23a53c884.hot-update.js": "/js/main.330cb26c0bc23a53c884.hot-update.js",
"/js/main.33a43c4c29198be879c5.hot-update.js": "/js/main.33a43c4c29198be879c5.hot-update.js",
"/js/main.60e7443fa7dee86c85d8.hot-update.js": "/js/main.60e7443fa7dee86c85d8.hot-update.js",
"/js/main.3e4d2dc845d6e6fd27a1.hot-update.js": "/js/main.3e4d2dc845d6e6fd27a1.hot-update.js",
"/js/main.c072ce5e1e8ee60ff114.hot-update.js": "/js/main.c072ce5e1e8ee60ff114.hot-update.js",
"/js/main.435a7170ae4c41a507bf.hot-update.js": "/js/main.435a7170ae4c41a507bf.hot-update.js",
"/js/main.3e5be2b0dcd70adc5f07.hot-update.js": "/js/main.3e5be2b0dcd70adc5f07.hot-update.js",
"/js/main.c1e5ceaa30c4a222234b.hot-update.js": "/js/main.c1e5ceaa30c4a222234b.hot-update.js",
"/js/main.430a9eb6b1382f42d8f6.hot-update.js": "/js/main.430a9eb6b1382f42d8f6.hot-update.js",
"/js/main.c962104d9cc0e244dbcf.hot-update.js": "/js/main.c962104d9cc0e244dbcf.hot-update.js",
"/js/main.631a573d0816d40f99b3.hot-update.js": "/js/main.631a573d0816d40f99b3.hot-update.js",
"/js/main.c4966c4a540e5ed0e0fe.hot-update.js": "/js/main.c4966c4a540e5ed0e0fe.hot-update.js",
"/js/main.a7e2cb4059892a5ce2d1.hot-update.js": "/js/main.a7e2cb4059892a5ce2d1.hot-update.js",
"/js/main.c1a278d80d8245b8dbed.hot-update.js": "/js/main.c1a278d80d8245b8dbed.hot-update.js",
"/js/main.6f0d43bf3f52d2c4ac14.hot-update.js": "/js/main.6f0d43bf3f52d2c4ac14.hot-update.js",
"/js/main.e784ffe94b54d5a998ab.hot-update.js": "/js/main.e784ffe94b54d5a998ab.hot-update.js",
"/js/main.dea1f26cd34f991d67e1.hot-update.js": "/js/main.dea1f26cd34f991d67e1.hot-update.js",
"/js/main.061ee62ce1874101ac2a.hot-update.js": "/js/main.061ee62ce1874101ac2a.hot-update.js",
"/js/main.85875fb8da3b6c2c9b22.hot-update.js": "/js/main.85875fb8da3b6c2c9b22.hot-update.js",
"/js/main.db7c288c6b109145c54f.hot-update.js": "/js/main.db7c288c6b109145c54f.hot-update.js",
"/js/main.70d8638e1b3444f4775f.hot-update.js": "/js/main.70d8638e1b3444f4775f.hot-update.js",
"/js/main.0d29c7bec145fcf354fb.hot-update.js": "/js/main.0d29c7bec145fcf354fb.hot-update.js",
"/js/main.9cd13670523ef48d28e6.hot-update.js": "/js/main.9cd13670523ef48d28e6.hot-update.js",
"/js/main.47ee00b321282e6ae987.hot-update.js": "/js/main.47ee00b321282e6ae987.hot-update.js",
"/js/main.5ef029b918e611550c36.hot-update.js": "/js/main.5ef029b918e611550c36.hot-update.js",
"/js/main.f6719ad9576c258cf584.hot-update.js": "/js/main.f6719ad9576c258cf584.hot-update.js",
"/js/main.e7ccbb9786d4116e87e5.hot-update.js": "/js/main.e7ccbb9786d4116e87e5.hot-update.js",
"/js/main.9ca0d30ea69682a73a3f.hot-update.js": "/js/main.9ca0d30ea69682a73a3f.hot-update.js",
"/js/main.aea23c89a4d4cb490f38.hot-update.js": "/js/main.aea23c89a4d4cb490f38.hot-update.js",
"/js/main.2a2df9603b1822c15a5e.hot-update.js": "/js/main.2a2df9603b1822c15a5e.hot-update.js",
"/js/main.489f68ca99fda16cb5d1.hot-update.js": "/js/main.489f68ca99fda16cb5d1.hot-update.js",
"/js/main.597a3d62f124fa4b1495.hot-update.js": "/js/main.597a3d62f124fa4b1495.hot-update.js",
"/js/main.d5392b120f1eb2e4c816.hot-update.js": "/js/main.d5392b120f1eb2e4c816.hot-update.js",
"/js/main.a7ae925207be44232c5c.hot-update.js": "/js/main.a7ae925207be44232c5c.hot-update.js",
"/js/main.bb42904cd41c18389ec8.hot-update.js": "/js/main.bb42904cd41c18389ec8.hot-update.js",
"/js/main.fe643a9998fdca67359e.hot-update.js": "/js/main.fe643a9998fdca67359e.hot-update.js",
"/js/main.daf27f047d4b8810e2cf.hot-update.js": "/js/main.daf27f047d4b8810e2cf.hot-update.js",
"/js/main.986dd7f8fc929c9c0b4f.hot-update.js": "/js/main.986dd7f8fc929c9c0b4f.hot-update.js",
"/js/main.77d2a6871e82cc5d0d09.hot-update.js": "/js/main.77d2a6871e82cc5d0d09.hot-update.js",
"/js/main.49faf863ade9028d0d34.hot-update.js": "/js/main.49faf863ade9028d0d34.hot-update.js",
"/js/main.0ffae012a756027e52bd.hot-update.js": "/js/main.0ffae012a756027e52bd.hot-update.js",
"/js/main.df15a1f233ee639ed364.hot-update.js": "/js/main.df15a1f233ee639ed364.hot-update.js",
"/js/main.3d8a3e25947961464dcc.hot-update.js": "/js/main.3d8a3e25947961464dcc.hot-update.js",
"/js/main.ef757ee6853c9f37b38c.hot-update.js": "/js/main.ef757ee6853c9f37b38c.hot-update.js",
"/js/main.009a2af69a329c5f4ced.hot-update.js": "/js/main.009a2af69a329c5f4ced.hot-update.js",
"/js/main.3eba90d0a4c80f1cc076.hot-update.js": "/js/main.3eba90d0a4c80f1cc076.hot-update.js",
"/js/main.530a0e768d44eaffc432.hot-update.js": "/js/main.530a0e768d44eaffc432.hot-update.js",
"/js/main.2e446e138db18ddc31a6.hot-update.js": "/js/main.2e446e138db18ddc31a6.hot-update.js",
"/js/main.d53c4114c93122303427.hot-update.js": "/js/main.d53c4114c93122303427.hot-update.js"
}

View File

@@ -74,7 +74,7 @@
'isLogged', 'isGuest'
]),
layout() {
if (includes(['PurchaseCode', 'StripeCredentials', 'AppSetup', 'EnvironmentSetup', 'BillingsDetail', 'SubscriptionPlans', 'Database', 'VerifyByPassword', 'SharedPage', 'NotFoundShared', 'SignIn', 'SignUp', 'ForgottenPassword', 'CreateNewPassword'], this.$route.name)) {
if (includes(['AdminAccount', 'PurchaseCode', 'SubscriptionService', 'StripeCredentials', 'AppSetup', 'EnvironmentSetup', 'BillingsDetail', 'SubscriptionPlans', 'Database', 'VerifyByPassword', 'SharedPage', 'NotFoundShared', 'SignIn', 'SignUp', 'ForgottenPassword', 'CreateNewPassword'], this.$route.name)) {
return 'unauthorized'
}

View File

@@ -14,7 +14,7 @@
/>
<div class="dropzone-message" v-show="! isData">
<upload-icon size="19" class="icon-upload"></upload-icon>
<image-icon size="28" class="icon-upload"></image-icon>
<span class="dropzone-title">
{{ $t('input_image.title') }}
</span>
@@ -26,7 +26,7 @@
</template>
<script>
import { UploadIcon } from 'vue-feather-icons'
import ImageIcon from "vue-feather-icons/icons/ImageIcon";
export default {
name: 'ImageInput',
@@ -34,7 +34,7 @@
'image', 'error'
],
components: {
UploadIcon
ImageIcon,
},
data() {
return {
@@ -86,7 +86,7 @@
text-align: center;
display: flex;
align-items: center;
min-height: 210px;
min-height: 175px;
&.is-error {
border: 2px dashed rgba(253, 57, 122, 0.3);
@@ -95,8 +95,10 @@
color: $danger;
}
.icon-upload path {
fill: $danger
.icon-upload {
rect, circle, polyline {
stroke: $danger
}
}
}
@@ -133,6 +135,12 @@
padding: 50px 0;
width: 100%;
.icon-upload {
rect, circle, polyline {
stroke: $theme
}
}
.dropzone-title {
@include font-size(16);
font-weight: 700;

View File

@@ -1,5 +1,5 @@
<template>
<div class="info-box">
<div class="info-box" :class="type">
<slot></slot>
</div>
</template>
@@ -7,6 +7,7 @@
<script>
export default {
name: 'InfoBox',
props: ['type']
}
</script>
@@ -21,9 +22,18 @@
background: $light_background;
text-align: left;
&.error {
background: rgba($danger, 0.1);
p, a {
color: $danger;
}
}
p {
@include font-size(15);
line-height: 1.6;
word-break: break-all;
}
a {

View File

@@ -59,10 +59,12 @@ import SetupWizard from './views/SetupWizard'
import Database from './views/SetupWizard/Database'
import AppSetup from './views/SetupWizard/AppSetup'
import PurchaseCode from './views/SetupWizard/PurchaseCode'
import AdminAccount from './views/SetupWizard/AdminAccount'
import BillingsDetail from './views/SetupWizard/BillingsDetail'
import EnvironmentSetup from './views/SetupWizard/EnvironmentSetup'
import StripeCredentials from './views/SetupWizard/StripeCredentials'
import SubscriptionPlans from './views/SetupWizard/SubscriptionPlans'
import SubscriptionService from './views/SetupWizard/SubscriptionService'
Vue.use(Router)
@@ -465,6 +467,14 @@ const routesMaintenance = [
requiresAuth: false,
},
},
{
name: 'SubscriptionService',
path: '/setup-wizard/subscription-service',
component: SubscriptionService,
meta: {
requiresAuth: false,
},
},
{
name: 'StripeCredentials',
path: '/setup-wizard/stripe-credentials',
@@ -475,7 +485,7 @@ const routesMaintenance = [
},
{
name: 'BillingsDetail',
path: '/setup-wizard/billings',
path: '/setup-wizard/stripe-billings',
component: BillingsDetail,
meta: {
requiresAuth: false,
@@ -483,7 +493,7 @@ const routesMaintenance = [
},
{
name: 'SubscriptionPlans',
path: '/setup-wizard/subscription-plans',
path: '/setup-wizard/stripe-plans',
component: SubscriptionPlans,
meta: {
requiresAuth: false,
@@ -505,6 +515,14 @@ const routesMaintenance = [
requiresAuth: false,
},
},
{
name: 'AdminAccount',
path: '/setup-wizard/admin-setup',
component: AdminAccount,
meta: {
requiresAuth: false,
},
},
]
},
]

View File

@@ -0,0 +1,202 @@
<template>
<AuthContentWrapper ref="auth">
<!--Database Credentials-->
<AuthContent name="database-credentials" :visible="true">
<div class="content-headline">
<settings-icon size="40" class="title-icon"></settings-icon>
<h1>Setup Wizard</h1>
<h2>Create your admin account.</h2>
</div>
<ValidationObserver @submit.prevent="adminAccountSubmit" ref="adminAccount" v-slot="{ invalid }" tag="form" class="form block-form">
<FormLabel>Create Admin Account</FormLabel>
<div class="block-wrapper">
<label>Avatar (optional):</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Avatar" v-slot="{ errors }">
<ImageInput v-model="admin.avatar" :error="errors[0]" />
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Full Name:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Full Name" rules="required" v-slot="{ errors }">
<input v-model="admin.name" placeholder="Type your full name" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Email:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Email" rules="required" v-slot="{ errors }">
<input v-model="admin.email" placeholder="Type your email" type="email" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Password:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Password" rules="required" v-slot="{ errors }">
<input v-model="admin.password" placeholder="Type your password" type="password" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Password Confirmation:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Password confirmation" rules="required" v-slot="{ errors }">
<input v-model="admin.password_confirmation" placeholder="Confirm your password" type="password" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="submit-wrapper">
<AuthButton icon="chevron-right" text="Create Admin and Login" :loading="isLoading" :disabled="isLoading"/>
</div>
</ValidationObserver>
</AuthContent>
</AuthContentWrapper>
</template>
<script>
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import AuthContentWrapper from '@/components/Auth/AuthContentWrapper'
import SelectInput from '@/components/Others/Forms/SelectInput'
import SwitchInput from '@/components/Others/Forms/SwitchInput'
import ImageInput from '@/components/Others/Forms/ImageInput'
import FormLabel from '@/components/Others/Forms/FormLabel'
import InfoBox from '@/components/Others/Forms/InfoBox'
import AuthContent from '@/components/Auth/AuthContent'
import AuthButton from '@/components/Auth/AuthButton'
import { SettingsIcon } from 'vue-feather-icons'
import {required} from 'vee-validate/dist/rules'
import {events} from "@/bus"
import axios from 'axios'
export default {
name: 'EnvironmentSetup',
components: {
AuthContentWrapper,
ValidationProvider,
ValidationObserver,
SettingsIcon,
SelectInput,
SwitchInput,
AuthContent,
ImageInput,
AuthButton,
FormLabel,
required,
InfoBox,
},
data() {
return {
isLoading: false,
admin: {
name: '',
email: '',
avatar: undefined,
password: '',
password_confirmation: '',
},
}
},
methods: {
async adminAccountSubmit() {
// Validate fields
const isValid = await this.$refs.adminAccount.validate();
if (!isValid) return;
// Start loading
this.isLoading = true
// Create form
let formData = new FormData()
// Add image to form
formData.append('name', this.admin.name)
formData.append('email', this.admin.email)
formData.append('password', this.admin.password)
formData.append('password_confirmation', this.admin.password_confirmation)
if (this.admin.avatar)
formData.append('avatar', this.admin.avatar)
axios
.post('/api/setup/admin-setup', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
})
.then(response => {
// End loading
this.isLoading = false
// Set login state
this.$store.commit('SET_AUTHORIZED', true)
// Go to files page
this.$router.push({name: 'Files'})
})
.catch(error => {
if (error.response.status == 401) {
if (error.response.data.error === 'invalid_client') {
events.$emit('alert:open', {
emoji: '🤔',
title: this.$t('popup_passport_error.title'),
message: this.$t('popup_passport_error.message')
})
}
}
if (error.response.status == 500) {
events.$emit('alert:open', {
emoji: '🤔',
title: this.$t('popup_signup_error.title'),
message: this.$t('popup_signup_error.message')
})
}
if (error.response.status == 422) {
if (error.response.data.errors['email']) {
this.$refs.adminAccount.setErrors({
'Email': error.response.data.errors['email']
});
}
if (error.response.data.errors['password']) {
this.$refs.adminAccount.setErrors({
'Password': error.response.data.errors['password']
});
}
}
// End loading
this.isLoading = false
})
},
},
created() {
var container = document.getElementById('vue-file-manager')
container.scrollTop = 0
}
}
</script>
<style scoped lang="scss">
@import '@assets/vue-file-manager/_forms';
@import '@assets/vue-file-manager/_auth';
@import '@assets/vue-file-manager/_setup_wizard';
</style>

View File

@@ -9,20 +9,97 @@
<h2>Set up your application appearance, analytics, etc.</h2>
</div>
<ValidationObserver @submit.prevent="appSetupSubmit" ref="appSetup" v-slot="{ invalid }" tag="form" class="form block-form">
<ValidationObserver @submit.prevent="appSetupSubmit" ref="appSetup" v-slot="{ invalid }" tag="form"
class="form block-form">
<FormLabel>General Settings</FormLabel>
<div class="block-wrapper">
<label>Mail Driver:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Mail Driver" rules="required" v-slot="{ errors }">
<input v-model="mail.driver" placeholder="Type your mail driver" type="text" />
<label>App Title:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="App Title" rules="required" v-slot="{ errors }">
<input v-model="app.title" placeholder="Type your app title" type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>App Description:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="App Description" rules="required" v-slot="{ errors }">
<input v-model="app.description" placeholder="Type your app description" type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>App Logo (optional):</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="App Logo" v-slot="{ errors }">
<ImageInput v-model="app.logo" :error="errors[0]"/>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>App Favicon (optional):</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="App Favicon" v-slot="{ errors }">
<ImageInput v-model="app.favicon" :error="errors[0]"/>
</ValidationProvider>
</div>
<FormLabel class="mt-70">Others Information</FormLabel>
<div class="block-wrapper">
<label>Contact Email:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Contact Email"
rules="required" v-slot="{ errors }">
<input v-model="app.contactMail" placeholder="Type your contact email" type="email" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Google Analytics Code (optional):</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Google Analytics Code"
v-slot="{ errors }">
<input v-model="app.googleAnalytics" placeholder="Paste your Google Analytics Code"
type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<div class="input-wrapper">
<div class="inline-wrapper">
<div class="switch-label">
<label class="input-label">Storage Limitation:</label>
</div>
<SwitchInput v-model="app.storageLimitation" class="switch" :state="app.storageLimitation"/>
</div>
</div>
</div>
<div class="block-wrapper" v-if="app.storageLimitation">
<label>Default Storage Space for Accounts:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Default Storage Space" rules="required" v-slot="{ errors }">
<input v-model="app.defaultStorage"
min="1"
max="999999999"
placeholder="Set default storage space in GB"
type="number"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<div class="input-wrapper">
<div class="inline-wrapper">
<div class="switch-label">
<label class="input-label">Allow User Registration:</label>
</div>
<SwitchInput v-model="app.userRegistration" class="switch" :state="app.userRegistration"/>
</div>
</div>
</div>
<div class="submit-wrapper">
<AuthButton icon="chevron-right" text="Save and Create Admin" :loading="isLoading" :disabled="isLoading"/>
</div>
@@ -36,11 +113,13 @@
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import AuthContentWrapper from '@/components/Auth/AuthContentWrapper'
import SelectInput from '@/components/Others/Forms/SelectInput'
import SwitchInput from '@/components/Others/Forms/SwitchInput'
import ImageInput from '@/components/Others/Forms/ImageInput'
import FormLabel from '@/components/Others/Forms/FormLabel'
import InfoBox from '@/components/Others/Forms/InfoBox'
import AuthContent from '@/components/Auth/AuthContent'
import AuthButton from '@/components/Auth/AuthButton'
import { SettingsIcon } from 'vue-feather-icons'
import {SettingsIcon} from 'vue-feather-icons'
import {required} from 'vee-validate/dist/rules'
import {mapGetters} from 'vuex'
import axios from 'axios'
@@ -53,7 +132,9 @@
ValidationObserver,
SettingsIcon,
SelectInput,
SwitchInput,
AuthContent,
ImageInput,
AuthButton,
FormLabel,
required,
@@ -62,51 +143,68 @@
data() {
return {
isLoading: false,
storageServiceList: [
{
label: 'Local Driver',
value: 'local',
},
{
label: 'Amazon Web Services S3',
value: 's3',
},
{
label: 'Digital Ocean Spaces',
value: 'spaces',
},
],
encryptionList: [
{
label: 'TLS',
value: 'tls',
},
{
label: 'SSL',
value: 'ssl',
},
],
storage: {
driver: 'local',
key: '',
secret: '',
endpoint: '',
region: '',
bucket: '',
app: {
title: '',
description: '',
logo: undefined,
favicon: undefined,
contactMail: '',
googleAnalytics: '',
defaultStorage: '',
userRegistration: 1,
storageLimitation: 1,
},
mail: {
driver: '',
host: '',
port: '',
username: '',
password: '',
encryption: '',
}
}
},
methods: {
async appSetupSubmit() {
this.$router.push({name: 'AppSetup'})
// Validate fields
const isValid = await this.$refs.appSetup.validate();
if (!isValid) return;
// Start loading
this.isLoading = true
// Create form
let formData = new FormData()
// Add image to form
formData.append('title', this.app.title)
formData.append('description', this.app.description)
formData.append('contactMail', this.app.contactMail)
formData.append('googleAnalytics', this.app.googleAnalytics)
formData.append('defaultStorage', this.app.defaultStorage)
formData.append('userRegistration', this.app.userRegistration)
formData.append('storageLimitation', this.app.storageLimitation)
if (this.app.logo)
formData.append('logo', this.app.logo)
if (this.app.favicon)
formData.append('favicon', this.app.favicon)
// Send request to get verify account
axios
.post('/api/setup/app-setup', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
})
.then(response => {
// End loading
this.isLoading = false
// Redirect to next step
this.$router.push({name: 'AdminAccount'})
})
.catch(error => {
// End loading
this.isLoading = false
})
},
},
created() {
@@ -117,7 +215,6 @@
</script>
<style scoped lang="scss">
//@import '@assets/vue-file-manager/_auth-form';
@import '@assets/vue-file-manager/_forms';
@import '@assets/vue-file-manager/_auth';
@import '@assets/vue-file-manager/_setup_wizard';

View File

@@ -6,7 +6,7 @@
<div class="content-headline">
<settings-icon size="40" class="title-icon"></settings-icon>
<h1>Setup Wizard</h1>
<h2>Set up you billing information.</h2>
<h2>Set up your billing information.</h2>
</div>
<ValidationObserver @submit.prevent="billingInformationSubmit" ref="billingInformation" v-slot="{ invalid }"
@@ -18,7 +18,7 @@
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Billing Name"
rules="required" v-slot="{ errors }">
<input v-model="billingInformation.billing_name" placeholder="Type your company name"
type="text"/>
type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -28,7 +28,7 @@
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Billing Vat Number"
rules="required" v-slot="{ errors }">
<input v-model="billingInformation.billing_vat_number" placeholder="Type your VAT number"
type="text"/>
type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -48,8 +48,8 @@
<label>Billing Address:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Billing Address"
rules="required" v-slot="{ errors }">
<input v-model="billingInformation.billing_address" placeholder="Select your billing address"
type="text"/>
<input v-model="billingInformation.billing_address" placeholder="Type your billing address"
type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -59,8 +59,8 @@
<label>Billing City:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Billing City"
rules="required" v-slot="{ errors }">
<input v-model="billingInformation.billing_city" placeholder="Select your billing city"
type="text"/>
<input v-model="billingInformation.billing_city" placeholder="Type your billing city"
type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -69,7 +69,7 @@
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Billing Postal Code"
rules="required" v-slot="{ errors }">
<input v-model="billingInformation.billing_postal_code"
placeholder="Select your billing postal code" type="text"/>
placeholder="Type your billing postal code" type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -79,8 +79,18 @@
<label>Billing State:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Billing State"
rules="required" v-slot="{ errors }">
<input v-model="billingInformation.billing_state" placeholder="Select your billing state"
type="text"/>
<input v-model="billingInformation.billing_state" placeholder="Type your billing state"
type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Billing Phone Number (optional):</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Billing Phone Number"
v-slot="{ errors }">
<input v-model="billingInformation.billing_phone_number" placeholder="Type your billing phone number"
type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -384,7 +394,31 @@
},
methods: {
async billingInformationSubmit() {
this.$router.push({name: 'SubscriptionPlans'})
// Validate fields
const isValid = await this.$refs.billingInformation.validate();
if (!isValid) return;
// Start loading
this.isLoading = true
// Send request to get verify account
axios
.post('/api/setup/stripe-billings', this.billingInformation)
.then(response => {
// End loading
this.isLoading = false
// Redirect to next step
this.$router.push({name: 'SubscriptionPlans'})
})
.catch(error => {
// End loading
this.isLoading = false
})
},
},
created() {

View File

@@ -6,24 +6,24 @@
<div class="content-headline">
<settings-icon size="40" class="title-icon"></settings-icon>
<h1>Setup Wizard</h1>
<h2>Set up your database credentials.</h2>
<h2>Set up your database connection to install application database.</h2>
</div>
<ValidationObserver @submit.prevent="databaseCredentialsSubmit" ref="verifyPurchaseCode" v-slot="{ invalid }" tag="form" class="form block-form">
<FormLabel>Database Credentials</FormLabel>
<InfoBox>
<p>Firstly, create your database credentials in your database client. For how to, here is Usefull resources:</p>
<p>We strongly recommend use MySQL or MariaDB database. Create new database and get credentials in your locale database client. For those who use cPanel or Plesk, here is useful resources:</p>
<ul>
<li>
<a href="#" target="_blank">1. cPanel - MySQL Database Wizard</a>
<a href="#" target="_blank">2. Plesk - Website databases</a>
<a href="https://www.inmotionhosting.com/support/edu/cpanel/create-database-2/" target="_blank">1. cPanel - MySQL Database Wizard</a>
<a href="https://docs.plesk.com/en-US/obsidian/customer-guide/65157/" target="_blank">2. Plesk - Website databases</a>
</li>
</ul>
</InfoBox>
<div class="block-wrapper">
<label>Connection:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="connection" rules="required" v-slot="{ errors }">
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Connection" rules="required" v-slot="{ errors }">
<SelectInput v-model="databaseCredentials.connection" :options="connectionList" default="mysql" placeholder="Select your database connection" :isError="errors[0]"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
@@ -31,38 +31,52 @@
<div class="block-wrapper">
<label>Host:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="connection" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.host" placeholder="Type your database host" type="text" />
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Host" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.host" placeholder="Type your database host" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Port:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="connection" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.port" placeholder="Type your database port" type="text" />
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Port" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.port" placeholder="Type your database port" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Database Name:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="connection" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.name" placeholder="Select your database name" type="text" />
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Database Name" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.name" placeholder="Select your database name" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Database Username:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Database Username" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.username" placeholder="Select your database name" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Database Password:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="connection" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.password" placeholder="Select your database password" type="text" />
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Database Password" rules="required" v-slot="{ errors }">
<input v-model="databaseCredentials.password" placeholder="Select your database password" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<InfoBox v-if="isError" type="error" style="margin-bottom: 10px">
<p>We couldn't establish database connection. Please double check your database credentials.</p>
<br>
<p>Detailed error: {{ errorMessage }}</p>
</InfoBox>
<div class="submit-wrapper">
<AuthButton icon="chevron-right" text="Test your Connection and Continue" :loading="isLoading" :disabled="isLoading"/>
<AuthButton icon="chevron-right" :text="submitButtonText" :loading="isLoading" :disabled="isLoading"/>
</div>
</ValidationObserver>
@@ -97,15 +111,22 @@
required,
InfoBox,
},
computed: {
submitButtonText() {
return this.isLoading ? 'Testing Database Connection' : 'Test your Connection and Continue'
}
},
data() {
return {
isLoading: false,
isError: false,
errorMessage: '',
connectionList: [
{
label: 'MySQL',
value: 'mysql',
},
{
/*{
label: 'SQLite',
value: 'sqlite',
},
@@ -116,20 +137,51 @@
{
label: 'SQLSry',
value: 'sqlsrv',
},
},*/
],
databaseCredentials: {
connection: '',
host: '',
port: '',
name: '',
password: '',
connection: 'mysql',
host: '127.0.0.1',
port: '8889',
name: 'file-manager',
username: 'root',
password: 'root',
}
}
},
methods: {
async databaseCredentialsSubmit() {
this.$router.push({name: 'StripeCredentials'})
// Validate fields
const isValid = await this.$refs.verifyPurchaseCode.validate();
if (!isValid) return;
// Start loading
this.isLoading = true
this.isError = false
// Send request to get verify account
axios
.post('/api/setup/database', this.databaseCredentials)
.then(response => {
// End loading
this.isLoading = false
// Redirect to next step
this.$router.push({name: 'SubscriptionService'})
})
.catch(error => {
if (error.response.status = 500) {
this.isError = true
this.errorMessage = error.response.data.message
}
// End loading
this.isLoading = false
})
},
},
created() {

View File

@@ -9,9 +9,9 @@
<h2>Set up your storage driver and email client.</h2>
</div>
<ValidationObserver @submit.prevent="EnvironmentSetupSubmit" ref="stripeCredentials" v-slot="{ invalid }" tag="form" class="form block-form">
<ValidationObserver @submit.prevent="EnvironmentSetupSubmit" ref="environmentSetup" v-slot="{ invalid }" tag="form" class="form block-form">
<InfoBox>
<p>If you dont know which storage set, select <b>'Local Driver'</b>. For more info, where
<p>If you dont know which storage driver set, keep selected <b>'Local Driver'</b>. For more info, where
you can host your files <a href="https://vuefilemanager.com/docs/guide/storage.html#introduction" target="_blank">visit our guide</a>.</p>
</InfoBox>
@@ -29,35 +29,35 @@
<div class="block-wrapper">
<label>Key:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Key" rules="required" v-slot="{ errors }">
<input v-model="storage.key" placeholder="Paste your key" type="text" />
<input v-model="storage.key" placeholder="Paste your key" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Secret:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Secret" rules="required" v-slot="{ errors }">
<input v-model="storage.secret" placeholder="Paste your secret" type="text" />
<input v-model="storage.secret" placeholder="Paste your secret" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<div class="block-wrapper" v-if="storage.driver !== 's3'">
<label>Endpoint:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Endpoint" rules="required" v-slot="{ errors }">
<input v-model="storage.endpoint" placeholder="Type your endpoint" type="text" />
<input v-model="storage.endpoint" placeholder="Type your endpoint" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Region:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Region" rules="required" v-slot="{ errors }">
<input v-model="storage.region" placeholder="Type your region" type="text" />
<input v-model="storage.region" placeholder="Type your region" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Bucket:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Bucket" rules="required" v-slot="{ errors }">
<input v-model="storage.bucket" placeholder="Type your bucket name" type="text" />
<input v-model="storage.bucket" placeholder="Type your bucket name" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -72,7 +72,7 @@
<div class="block-wrapper">
<label>Mail Driver:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Mail Driver" rules="required" v-slot="{ errors }">
<input v-model="mail.driver" placeholder="Type your mail driver" type="text" />
<input v-model="mail.driver" placeholder="Type your mail driver" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -80,7 +80,7 @@
<div class="block-wrapper">
<label>Mail Host:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Mail Host" rules="required" v-slot="{ errors }">
<input v-model="mail.host" placeholder="Type your mail host" type="text" />
<input v-model="mail.host" placeholder="Type your mail host" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -88,7 +88,7 @@
<div class="block-wrapper">
<label>Mail Port:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Mail Port" rules="required" v-slot="{ errors }">
<input v-model="mail.port" placeholder="Type your mail port" type="text" />
<input v-model="mail.port" placeholder="Type your mail port" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -96,7 +96,7 @@
<div class="block-wrapper">
<label>Mail Username:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Mail Username" rules="required" v-slot="{ errors }">
<input v-model="mail.username" placeholder="Type your mail username" type="text" />
<input v-model="mail.username" placeholder="Type your mail username" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -104,7 +104,7 @@
<div class="block-wrapper">
<label>Mail Password:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Mail Password" rules="required" v-slot="{ errors }">
<input v-model="mail.password" placeholder="Type your mail password" type="text" />
<input v-model="mail.password" placeholder="Type your mail password" type="text" :class="{'is-error': errors[0]}" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -118,7 +118,7 @@
</div>
<div class="submit-wrapper">
<AuthButton icon="chevron-right" text="Save and Set Billings" :loading="isLoading" :disabled="isLoading"/>
<AuthButton icon="chevron-right" text="Save and Set General Settings" :loading="isLoading" :disabled="isLoading"/>
</div>
</ValidationObserver>
@@ -169,6 +169,14 @@
label: 'Digital Ocean Spaces',
value: 'spaces',
},
{
label: 'Object Cloud Storage by Wasabi',
value: 'wasabi',
},
{
label: 'Backblaze B2 Cloud Storage',
value: 'backblaze',
},
],
encryptionList: [
{
@@ -200,7 +208,34 @@
},
methods: {
async EnvironmentSetupSubmit() {
this.$router.push({name: 'AppSetup'})
// Validate fields
const isValid = await this.$refs.environmentSetup.validate();
if (!isValid) return;
// Start loading
this.isLoading = true
// Send request to get verify account
axios
.post('/api/setup/environment-setup', {
storage: this.storage,
mail: this.mail,
})
.then(response => {
// End loading
this.isLoading = false
// Redirect to next step
this.$router.push({name: 'AppSetup'})
})
.catch(error => {
// End loading
this.isLoading = false
})
},
},
created() {

View File

@@ -7,12 +7,12 @@
<div class="content-headline">
<settings-icon size="40" class="title-icon"></settings-icon>
<h1>Setup Wizard</h1>
<h2>Please set up your application before continue.</h2>
<h2>Please verify your purchase code before continue to set up your application.</h2>
</div>
<ValidationObserver @submit.prevent="verifyPurchaseCode" ref="verifyPurchaseCode" v-slot="{ invalid }" tag="form" class="form inline-form">
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="PurchaseCode" rules="required" v-slot="{ errors }">
<input v-model="licence.purchaseCode" placeholder="Paste your purchase code" type="text" :class="{'is-error': errors[0]}"/>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Purchase Code" rules="required" v-slot="{ errors }">
<input v-model="purchaseCode" placeholder="Paste your purchase code" type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
<AuthButton icon="chevron-right" text="Verify" :loading="isLoading" :disabled="isLoading"/>
@@ -22,7 +22,9 @@
<a href="https://help.market.envato.com/hc/en-us/articles/202822600-Where-Is-My-Purchase-Code-" target="_blank">
Where I can find purchase code?
</a>
Dont have purchase code?
<a class="black-link" href="https://codecanyon.net/item/vue-file-manager-with-laravel-backend/25815986" target="_blank">
Dont have purchase code?
</a>
</p>
</AuthContent>
</AuthContentWrapper>
@@ -31,6 +33,7 @@
<script>
import AuthContentWrapper from '@/components/Auth/AuthContentWrapper'
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import InfoBox from '@/components/Others/Forms/InfoBox'
import AuthContent from '@/components/Auth/AuthContent'
import AuthButton from '@/components/Auth/AuthButton'
import { SettingsIcon } from 'vue-feather-icons'
@@ -48,22 +51,17 @@
AuthContent,
AuthButton,
required,
InfoBox,
},
data() {
return {
isLoading: false,
licence: {
purchaseCode: ''
},
purchaseCode: 'e3420e63-ce6f-4d04-9b3e-f7f5cc6af7c6'
}
},
methods: {
async verifyPurchaseCode() {
this.$router.push({name: 'Database'})
return
// Validate fields
const isValid = await this.$refs.verifyPurchaseCode.validate();
@@ -74,22 +72,31 @@
// Send request to get verify account
axios
.post('/api/setup-wizard', {
step: this.step,
data: this.stepLicence,
.post('/api/setup/purchase-code', {
purchaseCode: this.purchaseCode,
})
.then(response => {
// End loading
this.isLoading = false
// Go to new page
this.step++
// Redirect to next step
this.$router.push({name: 'Database'})
})
.catch(error => {
// End loading
this.isLoading = false
if (error.response.status == 400) {
this.$refs.verifyPurchaseCode.setErrors({
'Purchase Code': ['Purchase code is invalid.']
});
} else {
this.$refs.verifyPurchaseCode.setErrors({
'Purchase Code': ['Something is wrong. Please try again.']
});
}
})
},
},
@@ -101,6 +108,13 @@
@import '@assets/vue-file-manager/_auth';
@import '@assets/vue-file-manager/_setup_wizard';
.additional-link {
.black-link {
color: $text;
}
}
.auth-form input {
min-width: 380px;
}

View File

@@ -11,7 +11,7 @@
<ValidationObserver @submit.prevent="stripeCredentialsSubmit" ref="stripeCredentials" v-slot="{ invalid }" tag="form" class="form block-form">
<InfoBox>
<p>If you dont have stripe account, please <a href="#" target="_blank">register here</a> and get your Stripe Key, Stripe Secret and Stripe Webhook Secret.</p>
<p>If you dont have stripe account, please <a href="#" target="_blank">register here</a> and get your Publishable Key, Secret Key and create your webhook.</p>
</InfoBox>
<FormLabel>Stripe Setup</FormLabel>
@@ -19,7 +19,7 @@
<div class="block-wrapper">
<label>Stripe Currency:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Currency" rules="required" v-slot="{ errors }">
<SelectInput v-model="stripeCredentials.currency" :options="currencyList" default="mysql" placeholder="Select your stripe currency" :isError="errors[0]"/>
<SelectInput v-model="stripeCredentials.currency" :options="currencyList" default="mysql" placeholder="Select your Stripe currency" :isError="errors[0]"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -27,39 +27,50 @@
<FormLabel class="mt-70">Stripe Credentials</FormLabel>
<div class="block-wrapper">
<label>Stripe Key:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Key" rules="required" v-slot="{ errors }">
<input v-model="stripeCredentials.key" placeholder="Type your stripe key" type="text" />
<label>Publishable Key:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Publishable Key" rules="required" v-slot="{ errors }">
<input v-model="stripeCredentials.key" placeholder="Paste your publishable key" type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Stripe Secret:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Secret" rules="required" v-slot="{ errors }">
<input v-model="stripeCredentials.secret" placeholder="Type your stripe secret" type="text" />
<label>Secret Key:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Secret Key" rules="required" v-slot="{ errors }">
<input v-model="stripeCredentials.secret" placeholder="Paste your secret key" type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Stripe Webhook Secret:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Webhook Secret" rules="required" v-slot="{ errors }">
<input v-model="stripeCredentials.webhookSecret" placeholder="Type your stripe webhook secret" type="text" />
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<FormLabel class="mt-70">Stripe Webhook</FormLabel>
<InfoBox>
<p>You have to create webhook endpoint in your Stripe Dashboard. You can find it in <b>Dashboard -> Developers -> Webhooks -> Add Endpoint</b>. In Endpoint URL
please copy and paste url bellow. Make sure, this url is your public domain, not localhost. In events section, please click on <b>receive all events</b>.
That's all.</p>
</InfoBox>
<div class="block-wrapper">
<label>Stripe Webhook URL:</label>
<label>Endpoint URL:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Webhook URL" rules="required" v-slot="{ errors }">
<input :value="config.host + '/stripe/webhook'" placeholder="" type="text" disabled />
<input :value="stripeWebhookEndpoint" type="text" disabled/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Webhook Secret:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Webhook Secret" rules="required" v-slot="{ errors }">
<input v-model="stripeCredentials.webhookSecret" placeholder="Type your stripe webhook secret" type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<InfoBox v-if="isError" type="error" style="margin-bottom: -20px">
<p>{{ errorMessage }}</p>
</InfoBox>
<div class="submit-wrapper">
<AuthButton icon="chevron-right" text="Save and Set Billings" :loading="isLoading" :disabled="isLoading"/>
<AuthButton icon="chevron-right" :text="submitButtonText" :loading="isLoading" :disabled="isLoading"/>
</div>
</ValidationObserver>
@@ -75,7 +86,7 @@
import InfoBox from '@/components/Others/Forms/InfoBox'
import AuthContent from '@/components/Auth/AuthContent'
import AuthButton from '@/components/Auth/AuthButton'
import { SettingsIcon } from 'vue-feather-icons'
import {SettingsIcon} from 'vue-feather-icons'
import {required} from 'vee-validate/dist/rules'
import {mapGetters} from 'vuex'
import axios from 'axios'
@@ -96,10 +107,18 @@
},
computed: {
...mapGetters(['config']),
stripeWebhookEndpoint() {
return this.config.host + '/stripe/webhook'
},
submitButtonText() {
return this.isLoading ? 'Testing Stripe Connection' : 'Save and Set Billings'
}
},
data() {
return {
isLoading: false,
isError: false,
errorMessage: '',
currencyList: [
{
label: 'USD - United States Dollar',
@@ -652,7 +671,36 @@
},
methods: {
async stripeCredentialsSubmit() {
this.$router.push({name: 'BillingsDetail'})
// Validate fields
const isValid = await this.$refs.stripeCredentials.validate();
if (!isValid) return;
// Start loading
this.isLoading = true
// Send request to get verify account
axios
.post('/api/setup/stripe-credentials', this.stripeCredentials)
.then(response => {
// End loading
this.isLoading = false
// Redirect to next step
this.$router.push({name: 'BillingsDetail'})
})
.catch(error => {
if (error.response.status = 401) {
this.isError = true
this.errorMessage = error.response.data.message
}
// End loading
this.isLoading = false
})
},
},
created() {

View File

@@ -9,7 +9,8 @@
<h2>Set up plans for your customers.</h2>
</div>
<ValidationObserver @submit.prevent="subscriptionPlansSubmit" ref="subscriptionPlans" v-slot="{ invalid }" tag="form" class="form block-form">
<ValidationObserver @submit.prevent="subscriptionPlansSubmit" ref="subscriptionPlans" v-slot="{ invalid }"
tag="form" class="form block-form">
<FormLabel>Create your plans</FormLabel>
<InfoBox>
<p>Your plans will be <b>sorted automatically</b> in ascent order by plan price.</p>
@@ -23,8 +24,8 @@
<label>Name:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Name"
rules="required" v-slot="{ errors }">
<input v-model="plan.name" placeholder="Type your plan name"
type="text"/>
<input v-model="plan.attributes.name" placeholder="Type your plan name"
type="text" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -32,8 +33,9 @@
<div class="block-wrapper">
<label>Description (optional):</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Description"
rules="required" v-slot="{ errors }">
<textarea v-model="plan.description" placeholder="Type your plan description"></textarea>
v-slot="{ errors }">
<textarea v-model="plan.attributes.description"
placeholder="Type your plan description" :class="{'is-error': errors[0]}"></textarea>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -42,7 +44,9 @@
<label>Price:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Price"
rules="required" v-slot="{ errors }">
<input v-model="plan.price" placeholder="Type your plan price" type="text"/>
<input v-model="plan.attributes.price" placeholder="Type your plan price" type="number"
step="0.01" min="1" max="99999"
:class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -51,7 +55,12 @@
<label>Storage Capacity:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Storage Capacity"
rules="required" v-slot="{ errors }">
<input v-model="plan.storage_capacity" placeholder="Type your storage capacity" type="text"/>
<input v-model="plan.attributes.capacity"
min="1"
max="999999999"
placeholder="Type storage capacity in GB"
type="number"
:class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
@@ -61,11 +70,13 @@
@click.native="addRow"
class="duplicator-add-button"
button-style="theme-solid"
>Add New Plan</ButtonBase>
>Add New Plan
</ButtonBase>
</div>
<div class="submit-wrapper">
<AuthButton icon="chevron-right" text="Save and Go Next" :loading="isLoading" :disabled="isLoading"/>
<AuthButton icon="chevron-right" :text="submitButtonText" :loading="isLoading"
:disabled="isLoading"/>
</div>
</ValidationObserver>
@@ -84,7 +95,7 @@
import AuthButton from '@/components/Auth/AuthButton'
import {SettingsIcon} from 'vue-feather-icons'
import {required} from 'vee-validate/dist/rules'
import { XIcon } from 'vue-feather-icons'
import {XIcon} from 'vue-feather-icons'
import {mapGetters} from 'vuex'
import axios from 'axios'
@@ -104,31 +115,68 @@
InfoBox,
XIcon,
},
computed: {
submitButtonText() {
return this.isLoading ? 'Creating Subscription Stripe Plans' : 'Save and Go Next'
}
},
data() {
return {
isLoading: false,
subscriptionPlans: [
{
id: 1,
name: '',
description: '',
price: '',
storage_capacity: '',
type: 'plan',
attributes: {
name: '',
description: '',
price: '',
capacity: '',
}
}
]
}
},
methods: {
async subscriptionPlansSubmit() {
this.$router.push({name: 'EnvironmentSetup'})
async subscriptionPlansSubmit() {
// Validate fields
const isValid = await this.$refs.subscriptionPlans.validate();
if (!isValid) return;
// Start loading
this.isLoading = true
// Send request to get verify account
axios
.post('/api/setup/stripe-plans', {
plans: this.subscriptionPlans
})
.then(response => {
// End loading
this.isLoading = false
// Redirect to next step
this.$router.push({name: 'EnvironmentSetup'})
})
.catch(error => {
// End loading
this.isLoading = false
})
},
addRow() {
this.subscriptionPlans.push({
id: Math.floor(Math.random() * 10000000),
name: '',
description: '',
price: '',
storage_capacity: '',
type: 'plans',
attributes: {
name: '',
description: '',
price: '',
capacity: '',
}
})
},
removeRow(plan) {

View File

@@ -0,0 +1,145 @@
<template>
<AuthContentWrapper ref="auth">
<!--Licence Verify-->
<AuthContent name="subscription-service" :visible="true">
<div class="content-headline">
<settings-icon size="40" class="title-icon"></settings-icon>
<h1>Setup Wizard</h1>
<h2>You can charge users for storage space by monthly billing plans. Please, select your charging service or skip this step if you don't want charge users:</h2>
</div>
<div class="services">
<router-link :to="{name: 'StripeCredentials'}" tag="div" class="service-card">
<img src="/assets/icons/stripe-service.svg" alt="Stripe" class="service-logo">
<div class="service-content">
<b class="service-title">Charging with Stripe</b>
<p class="service-description">You can create custom storage plans and charge your users with monthly subscription.</p>
</div>
<router-link :to="{name: 'StripeCredentials'}" class="service-link">
<span>Set Up Billing and Plans With Stripe</span>
<chevron-right-icon size="22" class="icon"></chevron-right-icon>
</router-link>
</router-link>
</div>
<p class="additional-link">
<router-link :to="{name: 'EnvironmentSetup'}">
<AuthButton class="skip-subscription-setup" icon="chevron-right" text="I will set up Stripe later" />
</router-link>
</p>
</AuthContent>
</AuthContentWrapper>
</template>
<script>
import AuthContentWrapper from '@/components/Auth/AuthContentWrapper'
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import AuthContent from '@/components/Auth/AuthContent'
import AuthButton from '@/components/Auth/AuthButton'
import { SettingsIcon, ChevronRightIcon } from 'vue-feather-icons'
import {required} from 'vee-validate/dist/rules'
import {mapGetters} from 'vuex'
import axios from 'axios'
export default {
name: 'SubscriptionService',
components: {
AuthContentWrapper,
ValidationProvider,
ValidationObserver,
ChevronRightIcon,
SettingsIcon,
AuthContent,
AuthButton,
required,
},
data() {
return {
isLoading: false,
}
},
}
</script>
<style scoped lang="scss">
@import '@assets/vue-file-manager/_auth-form';
@import '@assets/vue-file-manager/_auth';
@import '@assets/vue-file-manager/_setup_wizard';
.services {
margin: 0 auto;
}
.service-card {
text-align: left;
box-shadow: 0 5px 30px 5px rgba(#3D4EFD, 0.25);
border-radius: 20px;
max-width: 415px;
display: inline-block;
padding: 30px;
background: rgb(58,75,255);
background: linear-gradient(135deg, rgba(58,75,255,1) 0%, rgba(103,114,229,1) 100%);
@include transition(200ms);
&:hover {
cursor: pointer;
box-shadow: 0 8px 35px 5px rgba(#3D4EFD, 0.4);
@include transform(scale(1.02));
}
.service-logo {
margin-bottom: 30px;
display: block;
}
.service-content {
margin-bottom: 65px;
.service-title {
@include font-size(18);
font-weight: 700;
color: white;
margin-bottom: 5px;
display: block;
}
.service-description {
@include font-size(16);
font-weight: 600;
color: white;
opacity: 0.8;
}
}
.service-link {
display: flex;
align-items: center;
.icon {
margin-left: 5px;
polyline {
stroke: white
}
}
span {
@include font-size(16);
font-weight: 700;
color: white;
}
}
}
.skip-subscription-setup {
border: none !important;
}
.auth-form input {
min-width: 380px;
}
</style>

View File

@@ -327,8 +327,6 @@
// If user don't have credit card, register new
if (!this.defaultPaymentMethod || this.payByNewCard) {
console.log('Payment by new card');
const {setupIntent, error} = await stripe.confirmCardSetup(this.clientSecret, {
payment_method: {
card: card,
@@ -367,8 +365,6 @@
// if user has credit card
if (this.defaultPaymentMethod && !this.payByNewCard) {
console.log('Payment by default card');
axios
.post('/api/subscription/upgrade', {
billing: this.billing,

View File

@@ -1,3 +1,9 @@
.content-headline {
max-width: 630px;
margin-left: auto;
margin-right: auto;
}
.auth-form {
input {

View File

@@ -16,6 +16,20 @@
|--------------------------------------------------------------------------
*/
// Setup Wizard
Route::group(['middleware' => ['api'], 'prefix' => 'setup'], function () {
Route::post('/purchase-code', 'General\SetupWizardController@verify_purchase_code');
Route::post('/database', 'General\SetupWizardController@setup_database');
Route::post('/stripe-credentials', 'General\SetupWizardController@store_stripe_credentials');
Route::post('/stripe-billings', 'General\SetupWizardController@store_stripe_billings');
Route::post('/stripe-plans', 'General\SetupWizardController@store_stripe_plans');
Route::post('/environment-setup', 'General\SetupWizardController@store_environment_setup');
Route::post('/app-setup', 'General\SetupWizardController@store_app_settings');
Route::post('/admin-setup', 'General\SetupWizardController@create_admin_account');
});
// Plans
Route::group(['middleware' => ['api'], 'prefix' => 'public'], function () {
Route::get('/pricing', 'General\PricingController@index');

View File

@@ -1,3 +0,0 @@
*
!data/
!.gitignore