diff --git a/src/.env.example b/src/.env.example --- a/src/.env.example +++ b/src/.env.example @@ -7,6 +7,7 @@ APP_DOMAIN=kolabnow.com APP_THEME=default APP_TENANT_ID=1 +APP_SIGNUP_APPROVAL=false ASSET_URL=http://127.0.0.1:8000 diff --git a/src/app/Auth/LDAPUserProvider.php b/src/app/Auth/LDAPUserProvider.php --- a/src/app/Auth/LDAPUserProvider.php +++ b/src/app/Auth/LDAPUserProvider.php @@ -51,7 +51,7 @@ { $authenticated = false; - if ($user->email === \strtolower($credentials['email'])) { + if ($user->email === \strtolower($credentials['email']) && !$user->isDraft()) { if (!empty($user->password)) { if (Hash::check($credentials['password'], $user->password)) { $authenticated = true; diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -228,6 +228,9 @@ $domain_name = Str::lower($domain_name); $domain = null; + // Are we using the signup with approval process? + $withApproval = \config('app.signup_approval'); + DB::beginTransaction(); // Create domain record @@ -243,6 +246,7 @@ $user = User::create([ 'email' => $login . '@' . $domain_name, 'password' => $request->password, + 'status' => $withApproval ? User::STATUS_DRAFT : 0, ]); if (!empty($discount)) { @@ -265,6 +269,10 @@ DB::commit(); + if ($withApproval) { + return response()->json(['status' => 'success']); + } + return AuthController::logonResponse($user); } diff --git a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php --- a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php @@ -11,6 +11,112 @@ class UsersController extends \App\Http\Controllers\API\V4\UsersController { + /** + * Approve the user signup (draft) + * + * @param \Illuminate\Http\Request $request The API request. + * @param string $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function approve(Request $request, $id) + { + $user = User::find($id); + $reseller = auth()->user(); + + if ( + empty($user) + || $user->tenant_id != $reseller->tenant_id + || $user->role == 'admin' + || !$user->isDraft() + ) { + return $this->errorResponse(404); + } + + $user->status ^= User::STATUS_DRAFT; + $user->save(); + + // FIXME: We should probably reset entitlements created_at/updated_at times to 'now' + // Also the user created_at too? + + \App\Jobs\SignupApprovalEmail::dispatch($user); + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-approve-success'), + ]); + } + + /** + * Dismiss the user signup (draft) + * + * @param \Illuminate\Http\Request $request The API request. + * @param string $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function dismiss(Request $request, $id) + { + $user = User::find($id); + $reseller = auth()->user(); + + if ( + empty($user) + || $user->tenant_id != $reseller->tenant_id + || $user->role == 'admin' + || !$user->isDraft() + ) { + return $this->errorResponse(404); + } + + $user->forceDelete(); + + // FIXME: Should we inform the user? + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-dismiss-success'), + ]); + } + + /** + * Returns users in a draft state. + * + * @return \Illuminate\Http\JsonResponse JSON response + */ + public function drafts() + { + $reseller = auth()->user(); + + $users = User::where('tenant_id', $reseller->tenant_id) + ->whereRaw('(status & ' . User::STATUS_DRAFT . ') > 0') + ->whereNull('role') + ->orderBy('created_at'); + + if (request()->input('count')) { + $count = $users->count(); + $users = []; + } else { + $users = $users->get(); + $count = count($users); + + $users->map(function ($user) { + return [ + 'id' => $user->id, + 'email' => $user->email, + 'name' => $user->name(), + 'external_email' => $user->getSetting('external_email'), + ]; + }); + } + + return response()->json([ + 'status' => 'success', + 'list' => $users, + 'count' => $count, + ]); + } + /** * Searching of user accounts. * diff --git a/src/app/Jobs/SignupApprovalEmail.php b/src/app/Jobs/SignupApprovalEmail.php new file mode 100644 --- /dev/null +++ b/src/app/Jobs/SignupApprovalEmail.php @@ -0,0 +1,28 @@ +getUser(); + + if (!$user) { + return; + } + + $email = $user->getSetting('external_email'); + + if (!$email) { + return; + } + + Mail::to($email)->send(new SignupApproval($user)); + } +} diff --git a/src/app/Mail/SignupApproval.php b/src/app/Mail/SignupApproval.php new file mode 100644 --- /dev/null +++ b/src/app/Mail/SignupApproval.php @@ -0,0 +1,67 @@ +user = $user; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $this->view('emails.html.signup_approval') + ->text('emails.plain.signup_approval') + ->subject(__('mail.signupapprval-subject', ['site' => \config('app.name')])) + ->with([ + 'site' => \config('app.name'), + 'username' => $this->user->name(true), + 'supportUrl' => \config('app.support_url'), + 'dashboardUrl' => Utils::serviceUrl('/dashboard'), + ]); + + return $this; + } + + /** + * Render the mail template with fake data + * + * @param string $type Output format ('html' or 'text') + * + * @return string HTML or Plain Text output + */ + public static function fakeRender(string $type = 'html'): string + { + $user = new User(); + + $mail = new self($user); + + return Helper::render($mail, $type); + } +} diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -43,6 +43,8 @@ public const STATUS_LDAP_READY = 1 << 4; // user mailbox has been created in IMAP public const STATUS_IMAP_READY = 1 << 5; + // user signup has not been approved yet + public const STATUS_DRAFT = 1 << 7; // change the default primary key type @@ -469,6 +471,16 @@ return ($this->status & self::STATUS_DELETED) > 0; } + /** + * Returns whether this user is a draft (not approved yet). + * + * @return bool + */ + public function isDraft(): bool + { + return ($this->status & self::STATUS_DRAFT) > 0; + } + /** * Returns whether this (external) domain has been verified * to exist in DNS. diff --git a/src/app/Wallet.php b/src/app/Wallet.php --- a/src/app/Wallet.php +++ b/src/app/Wallet.php @@ -60,6 +60,10 @@ public function chargeEntitlements($apply = true) { + if ($this->owner->isDraft()) { + return 0; + } + // This wallet has been created less than a month ago, this is the trial period if ($this->owner->created_at >= Carbon::now()->subMonthsWithoutOverflow(1)) { // Move all the current entitlement's updated_at timestamps forward to one month after diff --git a/src/config/app.php b/src/config/app.php --- a/src/config/app.php +++ b/src/config/app.php @@ -67,6 +67,8 @@ 'tenant_id' => env('APP_TENANT_ID', null), + 'signup_approval' => env('APP_SIGNUP_APPROVAL', false), + /* |-------------------------------------------------------------------------- | Application Domain diff --git a/src/resources/js/reseller/routes.js b/src/resources/js/reseller/routes.js --- a/src/resources/js/reseller/routes.js +++ b/src/resources/js/reseller/routes.js @@ -3,6 +3,7 @@ import LoginComponent from '../../vue/Login' import LogoutComponent from '../../vue/Logout' import PageComponent from '../../vue/Page' +import SignupsComponent from '../../vue/Reseller/Signups' //import StatsComponent from '../../vue/Reseller/Stats' //import UserComponent from '../../vue/Reseller/User' @@ -35,6 +36,12 @@ name: 'logout', component: LogoutComponent }, + { + path: '/signups', + name: 'signups', + component: SignupsComponent, + meta: { requiresAuth: true } + }, /* { path: '/stats', diff --git a/src/resources/lang/en/mail.php b/src/resources/lang/en/mail.php --- a/src/resources/lang/en/mail.php +++ b/src/resources/lang/en/mail.php @@ -71,6 +71,10 @@ 'support' => "Special circumstances? Something is wrong with a charge?\n" . ":site Support is here to help.", + 'signupapproval-subject' => ":site Signup Approved", + 'signupapproval-body' => "Thank you for signing up for a :site account.\n" + . "Your request has been approved. You can now log in to the cockpit (link below)."; + 'signupcode-subject' => ":site Registration", 'signupcode-body1' => "This is your verification code for the :site registration process:", 'signupcode-body2' => "You can also click the link below to continue the registration process:", diff --git a/src/resources/views/emails/html/signup_approval.blade.php b/src/resources/views/emails/html/signup_approval.blade.php new file mode 100644 --- /dev/null +++ b/src/resources/views/emails/html/signup_approval.blade.php @@ -0,0 +1,16 @@ + + + + + + +

{{ __('mail.header', ['name' => $username]) }}

+ +

{{ __('mail.signupapproval-body', ['site' => $site]) }}

+ +

{!! $dashboardUrl !!}

+ +

{{ __('mail.footer1') }}

+

{{ __('mail.footer2', ['site' => $site]) }}

+ + diff --git a/src/resources/views/emails/plain/signup_approval.blade.php b/src/resources/views/emails/plain/signup_approval.blade.php new file mode 100644 --- /dev/null +++ b/src/resources/views/emails/plain/signup_approval.blade.php @@ -0,0 +1,9 @@ +{!! __('mail.header', ['name' => $username]) !!} + +{!! __('mail.signupapproval-body', ['site' => $site]) !!} + +{!! $dashboardUrl !!} + +-- +{!! __('mail.footer1') !!} +{!! __('mail.footer2', ['site' => $site]) !!} diff --git a/src/resources/vue/Reseller/Dashboard.vue b/src/resources/vue/Reseller/Dashboard.vue --- a/src/resources/vue/Reseller/Dashboard.vue +++ b/src/resources/vue/Reseller/Dashboard.vue @@ -1,11 +1,31 @@ diff --git a/src/resources/vue/Reseller/Signups.vue b/src/resources/vue/Reseller/Signups.vue new file mode 100644 --- /dev/null +++ b/src/resources/vue/Reseller/Signups.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue --- a/src/resources/vue/Signup.vue +++ b/src/resources/vue/Signup.vue @@ -94,6 +94,16 @@ + +
+
+

Sign Up - Wait for approval

+

+ Thank you for signing up for a {{ appName }} account. Your request now would have to be approved by us. + We'll send you an email notification when that happens. Stay tuned. +

+
+
@@ -101,6 +111,7 @@ export default { data() { return { + appName: window.config['app.name'], email: '', first_name: '', last_name: '', @@ -229,8 +240,13 @@ password_confirmation: this.password_confirmation, voucher: this.voucher }).then(response => { - // auto-login and goto dashboard - this.$root.loginUser(response.data) + if (response.data.access_token) { + // auto-login and goto dashboard + this.$root.loginUser(response.data) + } else { + // display confirmation (step 4 - waiting for approval) + this.displayForm(4) + } }) }, // Moves the user a step back in registration form @@ -246,7 +262,7 @@ } }, displayForm(step, focus) { - [0, 1, 2, 3].filter(value => value != step).forEach(value => { + [0, 1, 2, 3, 4].filter(value => value != step).forEach(value => { $('#step' + value).addClass('d-none') }) diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -174,7 +174,10 @@ Route::apiResource('entitlements', API\V4\Reseller\EntitlementsController::class); Route::apiResource('packages', API\V4\Reseller\PackagesController::class); Route::apiResource('skus', API\V4\Reseller\SkusController::class); + Route::get('users/drafts', 'API\V4\Reseller\UsersController@drafts'); Route::apiResource('users', API\V4\Reseller\UsersController::class); + Route::post('users/{id}/approve', 'API\V4\Admin\UsersController@approve'); + Route::post('users/{id}/dismiss', 'API\V4\Admin\UsersController@dismiss'); Route::apiResource('wallets', API\V4\Reseller\WalletsController::class); Route::apiResource('discounts', API\V4\Reseller\DiscountsController::class); }