Page MenuHomePhorge

D2377.1775687343.diff
No OneTemporary

Authored By
Unknown
Size
18 KB
Referenced Files
None
Subscribers
None

D2377.1775687343.diff

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 @@
+<?php
+
+namespace App\Jobs;
+
+class SignupApprovalEmail extends UserJob
+{
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $user = $this->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 @@
+<?php
+
+namespace App\Mail;
+
+use App\User;
+use App\Utils;
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+
+class SignupApproval extends Mailable
+{
+ use Queueable;
+ use SerializesModels;
+
+ /** @var \App\User A user that has been approved */
+ protected $user;
+
+
+ /**
+ * Create a new message instance.
+ *
+ * @param \App\User $user A user object
+ *
+ * @return void
+ */
+ public function __construct(User $user)
+ {
+ $this->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 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>{{ __('mail.header', ['name' => $username]) }}</p>
+
+ <p>{{ __('mail.signupapproval-body', ['site' => $site]) }}</p>
+
+ <p><a href="{!! $dashboardUrl !!}">{!! $dashboardUrl !!}</a></p>
+
+ <p>{{ __('mail.footer1') }}</p>
+ <p>{{ __('mail.footer2', ['site' => $site]) }}</p>
+ </body>
+</html>
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 @@
<template>
<div class="container" dusk="dashboard-component">
<div id="dashboard-nav" class="mt-3">
+ <router-link class="card link-signups" :to="{ name: 'signups' }">
+ <svg-icon icon="users"></svg-icon><span class="name">Signups</span>
+ <span v-if="signups > 0" class="badge badge-danger">{{ signups }}</span>
+ </router-link>
</div>
</div>
</template>
<script>
export default {
+ data() {
+ return {
+ signups: 0
+ }
+ },
+ mounted() {
+ this.countSignups()
+ },
+ methods: {
+ countSignups() {
+ axios.get('/api/v4/users/drafts?count=1')
+ .then(response => {
+ this.signups = response.data.count
+ })
+ }
+ }
}
</script>
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 @@
+<template>
+ <div class="container">
+ <div class="card" id="signups-list">
+ <div class="card-body">
+ <div class="card-title">Signups</div>
+ <div class="card-text">
+ <table class="table table-sm table-hover">
+ <thead class="thead-light">
+ <tr>
+ <th scope="col">Email</th>
+ <th scope="col">External Email</th>
+ <th scope="col">Name</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="user in users" :id="'user' + user.id" :key="user.id">
+ <td><svg-icon icon="user"></svg-icon> {{ user.email }}</td>
+ <td>{{ user.external_email }}</td>
+ <td>{{ user.name }}</td>
+ <td class="buttons">
+ <button class="btn btn-sm btn-success" @click="approveUser(user.id)">Approve</button>
+ <button class="btn btn-sm btn-danger" @click="dismissUser(user.id)">Dismiss</button>
+ </td>
+ </tr>
+ </tbody>
+ <tfoot class="table-fake-body">
+ <tr>
+ <td colspan="4">There are no new signups for approval.</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ users: []
+ }
+ },
+ created() {
+ this.$root.startLoading()
+
+ axios.get('/api/v4/users/drafts')
+ .then(response => {
+ this.$root.stopLoading()
+ this.users = response.data.list
+ })
+ .catch(this.$root.errorHandler)
+ },
+ methods: {
+ approveUser(id) {
+ axios.post('/api/v4/users/' + id + '/approve')
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$toast.success(response.data.message)
+ $('#user' + id).remove()
+ }
+ })
+ },
+ dismissUser(id) {
+
+ }
+ }
+ }
+</script>
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 @@
</form>
</div>
</div>
+
+ <div class="card d-none" id="step4">
+ <div class="card-body">
+ <h4 class="card-title">Sign Up - Wait for approval</h4>
+ <p class="card-text">
+ 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.
+ </p>
+ </div>
+ </div>
</div>
</template>
@@ -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);
}

File Metadata

Mime Type
text/plain
Expires
Wed, Apr 8, 10:29 PM (4 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18848581
Default Alt Text
D2377.1775687343.diff (18 KB)

Event Timeline