Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F118225199
D2377.1775695104.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
18 KB
Referenced Files
None
Subscribers
None
D2377.1775695104.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Thu, Apr 9, 12:38 AM (12 h, 33 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18848581
Default Alt Text
D2377.1775695104.diff (18 KB)
Attached To
Mode
D2377: [WIP] Resellers: Signup with approval
Attached
Detach File
Event Timeline