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]) }}
+ + + +{{ __('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 @@External Email | +Name | ++ | |
---|---|---|---|
{{ user.external_email }} | +{{ user.name }} | ++ + + | +|
There are no new signups 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. +
+