Page MenuHomePhorge

D1129.1774854057.diff
No OneTemporary

Authored By
Unknown
Size
63 KB
Referenced Files
None
Subscribers
None

D1129.1774854057.diff

diff --git a/docker/kolab/utils/15-create-hosted-domain.sh b/docker/kolab/utils/15-create-hosted-domain.sh
--- a/docker/kolab/utils/15-create-hosted-domain.sh
+++ b/docker/kolab/utils/15-create-hosted-domain.sh
@@ -54,7 +54,7 @@
(
for role in "2fa-user" "activesync-user" "imap-user"; do
- echo "cn=${role},${hosted_domain_rootdn}"
+ echo "dn: cn=${role},${hosted_domain_rootdn}"
echo "cn: ${role}"
echo "description: ${role} role"
echo "objectclass: top"
diff --git a/src/app/Auth/SecondFactor.php b/src/app/Auth/SecondFactor.php
--- a/src/app/Auth/SecondFactor.php
+++ b/src/app/Auth/SecondFactor.php
@@ -231,7 +231,7 @@
if (!isset($this->cache[$key])) {
$factors = $this->getFactors();
- $this->cache[$key] = $factors[$key];
+ $this->cache[$key] = isset($factors[$key]) ? $factors[$key] : null;
}
return $this->cache[$key];
diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php
--- a/src/app/Backends/LDAP.php
+++ b/src/app/Backends/LDAP.php
@@ -309,7 +309,10 @@
}
if (!array_key_exists('nsroledn', $oldEntry)) {
- $oldEntry['nsroledn'] = (array)$ldap->get_entry_attributes($dn, ['nsroledn']);
+ $roles = $ldap->get_entry_attributes($dn, ['nsroledn']);
+ if (!empty($roles)) {
+ $oldEntry['nsroledn'] = (array)$roles['nsroledn'];
+ }
}
$newEntry = $oldEntry;
@@ -385,56 +388,48 @@
$entry['mailquota'] = 0;
- if (!array_key_exists('nsroledn', $entry)) {
- $entry['nsroledn'] = [];
- } else if (!is_array($entry['nsroledn'])) {
- $entry['nsroledn'] = (array)$entry['nsroledn'];
- }
-
$roles = [];
foreach ($user->entitlements as $entitlement) {
\Log::debug("Examining {$entitlement->sku->title}");
switch ($entitlement->sku->title) {
+ case "mailbox":
+ break;
+
case "storage":
$entry['mailquota'] += 1048576;
break;
- }
- $roles[] = $entitlement->sku->title;
+ default:
+ $roles[] = $entitlement->sku->title;
+ break;
+ }
}
$hostedRootDN = \config('ldap.hosted.root_dn');
+ if (empty($roles)) {
+ if (array_key_exists('nsroledn', $entry)) {
+ unset($entry['nsroledn']);
+ }
+
+ return;
+ }
+
+ $entry['nsroledn'] = [];
+
if (in_array("2fa", $roles)) {
$entry['nsroledn'][] = "cn=2fa-user,{$hostedRootDN}";
- } else {
- $key = array_search("cn=2fa-user,{$hostedRootDN}", $entry['nsroledn']);
- if ($key !== false) {
- unset($entry['nsroledn'][$key]);
- }
}
if (in_array("activesync", $roles)) {
$entry['nsroledn'][] = "cn=activesync-user,{$hostedRootDN}";
- } else {
- $key = array_search("cn=activesync-user,{$hostedRootDN}", $entry['nsroledn']);
- if ($key !== false) {
- unset($entry['nsroledn'][$key]);
- }
}
if (!in_array("groupware", $roles)) {
$entry['nsroledn'][] = "cn=imap-user,{$hostedRootDN}";
- } else {
- $key = array_search("cn=imap-user,{$hostedRootDN}", $entry['nsroledn']);
- if ($key !== false) {
- unset($entry['nsroledn'][$key]);
- }
}
-
- $entry['nsroledn'] = array_unique($entry['nsroledn']);
}
/**
diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -350,13 +350,22 @@
return true;
}
- $record = \dns_get_record($this->namespace, DNS_ANY);
+ $records = \dns_get_record($this->namespace, DNS_ANY);
- if ($record === false) {
+ if ($records === false) {
throw new \Exception("Failed to get DNS record for {$this->namespace}");
}
- if (!empty($record)) {
+ // It may happen that result contains other domains depending on the host
+ // DNS setup
+ $hosts = array_map(
+ function ($record) {
+ return $record['host'];
+ },
+ $records
+ );
+
+ if (in_array($this->namespace, $hosts)) {
$this->status |= Domain::STATUS_VERIFIED;
$this->save();
diff --git a/src/app/Http/Controllers/API/AuthController.php b/src/app/Http/Controllers/API/AuthController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/AuthController.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use App\User;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Validator;
+
+class AuthController extends Controller
+{
+ /**
+ * Get the authenticated User
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function info()
+ {
+ $user = $this->guard()->user();
+ $response = V4\UsersController::userResponse($user);
+
+ return response()->json($response);
+ }
+
+ /**
+ * Helper method for other controllers with user auto-logon
+ * functionality
+ *
+ * @param \App\User $user User model object
+ */
+ public static function logonResponse(User $user)
+ {
+ $token = auth()->login($user);
+
+ return response()->json([
+ 'status' => 'success',
+ 'access_token' => $token,
+ 'token_type' => 'bearer',
+ 'expires_in' => Auth::guard()->factory()->getTTL() * 60,
+ ]);
+ }
+
+ /**
+ * Get a JWT token via given credentials.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function login(Request $request)
+ {
+ // TODO: Redirect to dashboard if authenticated.
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'email' => 'required|min:2',
+ 'password' => 'required|min:4',
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ $credentials = $request->only('email', 'password');
+
+ if ($token = $this->guard()->attempt($credentials)) {
+ $sf = new \App\Auth\SecondFactor($this->guard()->user());
+
+ if ($response = $sf->requestHandler($request)) {
+ return $response;
+ }
+
+ return $this->respondWithToken($token);
+ }
+
+ return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401);
+ }
+
+ /**
+ * Log the user out (Invalidate the token)
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function logout()
+ {
+ $this->guard()->logout();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => __('auth.logoutsuccess')
+ ]);
+ }
+
+ /**
+ * Refresh a token.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function refresh()
+ {
+ return $this->respondWithToken($this->guard()->refresh());
+ }
+
+ /**
+ * Get the token array structure.
+ *
+ * @param string $token Respond with this token.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ protected function respondWithToken($token)
+ {
+ return response()->json(
+ [
+ 'access_token' => $token,
+ 'token_type' => 'bearer',
+ 'expires_in' => $this->guard()->factory()->getTTL() * 60
+ ]
+ );
+ }
+
+ /**
+ * Get the guard to be used during authentication.
+ *
+ * @return \Illuminate\Contracts\Auth\Guard
+ */
+ public function guard()
+ {
+ return Auth::guard();
+ }
+}
diff --git a/src/app/Http/Controllers/API/PasswordResetController.php b/src/app/Http/Controllers/API/PasswordResetController.php
--- a/src/app/Http/Controllers/API/PasswordResetController.php
+++ b/src/app/Http/Controllers/API/PasswordResetController.php
@@ -138,6 +138,6 @@
// Remove the verification code
$this->code->delete();
- return UsersController::logonResponse($user);
+ return AuthController::logonResponse($user);
}
}
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
@@ -232,7 +232,7 @@
DB::commit();
- return UsersController::logonResponse($user);
+ return AuthController::logonResponse($user);
}
/**
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Admin;
+
+class DomainsController extends \App\Http\Controllers\API\V4\DomainsController
+{
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/EntitlementsController.php b/src/app/Http/Controllers/API/V4/Admin/EntitlementsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Admin/EntitlementsController.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Admin;
+
+class EntitlementsController extends \App\Http\Controllers\API\V4\EntitlementsController
+{
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/PackagesController.php b/src/app/Http/Controllers/API/V4/Admin/PackagesController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Admin/PackagesController.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Admin;
+
+class PackagesController extends \App\Http\Controllers\API\V4\PackagesController
+{
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/SkusController.php b/src/app/Http/Controllers/API/V4/Admin/SkusController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Admin/SkusController.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Admin;
+
+class SkusController extends \App\Http\Controllers\API\V4\SkusController
+{
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Admin;
+
+class UsersController extends \App\Http\Controllers\API\V4\UsersController
+{
+ public function index()
+ {
+ $result = \App\User::orderBy('email')->get()->map(function ($user) {
+ $data = $user->toArray();
+ $data = array_merge($data, self::userStatuses($user));
+ return $data;
+ });
+
+ return response()->json($result);
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Admin;
+
+class WalletsController extends \App\Http\Controllers\API\V4\WalletsController
+{
+}
diff --git a/src/app/Http/Controllers/API/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
rename from src/app/Http/Controllers/API/DomainsController.php
rename to src/app/Http/Controllers/API/V4/DomainsController.php
--- a/src/app/Http/Controllers/API/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -1,6 +1,6 @@
<?php
-namespace App\Http\Controllers\API;
+namespace App\Http\Controllers\API\V4;
use App\Domain;
use App\Http\Controllers\Controller;
diff --git a/src/app/Http/Controllers/API/EntitlementsController.php b/src/app/Http/Controllers/API/V4/EntitlementsController.php
rename from src/app/Http/Controllers/API/EntitlementsController.php
rename to src/app/Http/Controllers/API/V4/EntitlementsController.php
--- a/src/app/Http/Controllers/API/EntitlementsController.php
+++ b/src/app/Http/Controllers/API/V4/EntitlementsController.php
@@ -1,6 +1,6 @@
<?php
-namespace App\Http\Controllers\API;
+namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
diff --git a/src/app/Http/Controllers/API/PackagesController.php b/src/app/Http/Controllers/API/V4/PackagesController.php
rename from src/app/Http/Controllers/API/PackagesController.php
rename to src/app/Http/Controllers/API/V4/PackagesController.php
--- a/src/app/Http/Controllers/API/PackagesController.php
+++ b/src/app/Http/Controllers/API/V4/PackagesController.php
@@ -1,6 +1,6 @@
<?php
-namespace App\Http\Controllers\API;
+namespace App\Http\Controllers\API\V4;
use App\Package;
use App\Http\Controllers\Controller;
diff --git a/src/app/Http/Controllers/API/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php
rename from src/app/Http/Controllers/API/PaymentsController.php
rename to src/app/Http/Controllers/API/V4/PaymentsController.php
--- a/src/app/Http/Controllers/API/PaymentsController.php
+++ b/src/app/Http/Controllers/API/V4/PaymentsController.php
@@ -1,6 +1,6 @@
<?php
-namespace App\Http\Controllers\API;
+namespace App\Http\Controllers\API\V4;
use App\Payment;
use App\Wallet;
diff --git a/src/app/Http/Controllers/API/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
rename from src/app/Http/Controllers/API/SkusController.php
rename to src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -1,6 +1,6 @@
<?php
-namespace App\Http\Controllers\API;
+namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
use App\Sku;
diff --git a/src/app/Http/Controllers/API/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
rename from src/app/Http/Controllers/API/UsersController.php
rename to src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -1,6 +1,6 @@
<?php
-namespace App\Http\Controllers\API;
+namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
use App\Domain;
@@ -17,36 +17,6 @@
class UsersController extends Controller
{
/**
- * Create a new API\UsersController instance.
- *
- * Ensures that the correct authentication middleware is applied except for /login
- *
- * @return void
- */
- public function __construct()
- {
- $this->middleware('auth:api', ['except' => ['login']]);
- }
-
- /**
- * Helper method for other controllers with user auto-logon
- * functionality
- *
- * @param \App\User $user User model object
- */
- public static function logonResponse(User $user)
- {
- $token = auth()->login($user);
-
- return response()->json([
- 'status' => 'success',
- 'access_token' => $token,
- 'token_type' => 'bearer',
- 'expires_in' => Auth::guard()->factory()->getTTL() * 60,
- ]);
- }
-
- /**
* Delete a user.
*
* @param int $id User identifier
@@ -83,6 +53,7 @@
*/
public function index()
{
+ \Log::debug("Regular API");
$user = $this->guard()->user();
$result = $user->users()->orderBy('email')->get()->map(function ($user) {
@@ -95,98 +66,6 @@
}
/**
- * Get the authenticated User
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function info()
- {
- $user = $this->guard()->user();
- $response = $this->userResponse($user);
-
- return response()->json($response);
- }
-
- /**
- * Get a JWT token via given credentials.
- *
- * @param \Illuminate\Http\Request $request The API request.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function login(Request $request)
- {
- $v = Validator::make(
- $request->all(),
- [
- 'email' => 'required|min:2',
- 'password' => 'required|min:4',
- ]
- );
-
- if ($v->fails()) {
- return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
- }
-
- $credentials = $request->only('email', 'password');
-
- if ($token = $this->guard()->attempt($credentials)) {
- $sf = new \App\Auth\SecondFactor($this->guard()->user());
-
- if ($response = $sf->requestHandler($request)) {
- return $response;
- }
-
- return $this->respondWithToken($token);
- }
-
- return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401);
- }
-
- /**
- * Log the user out (Invalidate the token)
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function logout()
- {
- $this->guard()->logout();
-
- return response()->json([
- 'status' => 'success',
- 'message' => __('auth.logoutsuccess')
- ]);
- }
-
- /**
- * Refresh a token.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function refresh()
- {
- return $this->respondWithToken($this->guard()->refresh());
- }
-
- /**
- * Get the token array structure.
- *
- * @param string $token Respond with this token.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- protected function respondWithToken($token)
- {
- return response()->json(
- [
- 'access_token' => $token,
- 'token_type' => 'bearer',
- 'expires_in' => $this->guard()->factory()->getTTL() * 60
- ]
- );
- }
-
- /**
* Display information on the user account specified by $id.
*
* @param int $id The account to show information for.
@@ -458,7 +337,7 @@
*
* @return array Response data
*/
- protected function userResponse(User $user): array
+ public static function userResponse(User $user): array
{
$response = $user->toArray();
@@ -562,7 +441,7 @@
if (empty($email)) {
$errors['email'] = \trans('validation.required', ['attribute' => 'email']);
- } elseif ($error = self::validateEmail($email, $controller, false)) {
+ } elseif ($error = \App\Utils::validateEmail($email, $controller, false)) {
$errors['email'] = $error;
}
}
@@ -582,7 +461,7 @@
// validate new aliases
if (
!in_array($alias, $existing_aliases)
- && ($error = self::validateEmail($alias, $controller, true))
+ && ($error = \App\Utils::validateEmail($alias, $controller, true))
) {
if (!isset($errors['aliases'])) {
$errors['aliases'] = [];
@@ -606,62 +485,4 @@
$settings = $request->only(array_keys($rules));
unset($settings['password'], $settings['aliases'], $settings['email']);
}
-
- /**
- * Email address (login or alias) validation
- *
- * @param string $email Email address
- * @param \App\User $user The account owner
- * @param bool $is_alias The email is an alias
- *
- * @return string Error message on validation error
- */
- protected static function validateEmail(string $email, User $user, bool $is_alias = false): ?string
- {
- $attribute = $is_alias ? 'alias' : 'email';
-
- if (strpos($email, '@') === false) {
- return \trans('validation.entryinvalid', ['attribute' => $attribute]);
- }
-
- list($login, $domain) = explode('@', $email);
-
- // Check if domain exists
- $domain = Domain::where('namespace', Str::lower($domain))->first();
-
- if (empty($domain)) {
- return \trans('validation.domaininvalid');
- }
-
- // Validate login part alone
- $v = Validator::make(
- [$attribute => $login],
- [$attribute => ['required', new UserEmailLocal(!$domain->isPublic())]]
- );
-
- if ($v->fails()) {
- return $v->errors()->toArray()[$attribute][0];
- }
-
- // Check if it is one of domains available to the user
- // TODO: We should have a helper that returns "flat" array with domain names
- // I guess we could use pluck() somehow
- $domains = array_map(
- function ($domain) {
- return $domain->namespace;
- },
- $user->domains()
- );
-
- if (!in_array($domain->namespace, $domains)) {
- return \trans('validation.entryexists', ['attribute' => 'domain']);
- }
-
- // Check if user with specified address already exists
- if (User::findByEmail($email)) {
- return \trans('validation.entryexists', ['attribute' => $attribute]);
- }
-
- return null;
- }
}
diff --git a/src/app/Http/Controllers/API/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php
rename from src/app/Http/Controllers/API/WalletsController.php
rename to src/app/Http/Controllers/API/V4/WalletsController.php
--- a/src/app/Http/Controllers/API/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/WalletsController.php
@@ -1,10 +1,6 @@
<?php
-/**
- * API\WalletsController
- */
-
-namespace App\Http\Controllers\API;
+namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
diff --git a/src/app/Http/Kernel.php b/src/app/Http/Kernel.php
--- a/src/app/Http/Kernel.php
+++ b/src/app/Http/Kernel.php
@@ -19,7 +19,7 @@
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
- \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
+ \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class
];
/**
@@ -52,6 +52,7 @@
* @var array
*/
protected $routeMiddleware = [
+ 'admin' => \App\Http\Middleware\AuthenticateAdmin::class,
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
@@ -73,9 +74,11 @@
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
+ \App\Http\Middleware\AuthenticateAdmin::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
+ \App\Http\Middleware\AuthenticateAdmin::class,
];
}
diff --git a/src/app/Http/Middleware/AuthenticateAdmin.php b/src/app/Http/Middleware/AuthenticateAdmin.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Middleware/AuthenticateAdmin.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+
+class AuthenticateAdmin
+{
+ /**
+ * Handle an incoming request.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ * @return mixed
+ */
+ public function handle($request, Closure $next)
+ {
+ $user = auth()->user();
+
+ if (!$user) {
+ abort(403, "Unauthorized");
+ }
+
+ if ($user->role !== "admin") {
+ abort(403, "Unauthorized");
+ }
+
+ return $next($request);
+ }
+}
diff --git a/src/app/Jobs/UserVerify.php b/src/app/Jobs/UserVerify.php
--- a/src/app/Jobs/UserVerify.php
+++ b/src/app/Jobs/UserVerify.php
@@ -44,6 +44,26 @@
*/
public function handle()
{
+ // Verify a mailbox sku is among the user entitlements.
+ $skuMailbox = \App\Sku::where('title', 'mailbox')->first();
+
+ if (!$skuMailbox) {
+ return;
+ }
+
+ $mailbox = \App\Entitlement::where(
+ [
+ 'sku_id' => $skuMailbox->id,
+ 'entitleable_id' => $this->user->id,
+ 'entitleable_type' => User::class
+ ]
+ )->first();
+
+ if (!$mailbox) {
+ return;
+ }
+
+ // The user has a mailbox
if (!$this->user->isImapReady()) {
if (IMAP::verifyAccount($this->user->email)) {
$this->user->status |= User::STATUS_IMAP_READY;
diff --git a/src/app/Jobs/WalletPayment.php b/src/app/Jobs/WalletPayment.php
--- a/src/app/Jobs/WalletPayment.php
+++ b/src/app/Jobs/WalletPayment.php
@@ -3,7 +3,7 @@
namespace App\Jobs;
use App\Wallet;
-use App\Http\Controllers\API\PaymentsController;
+use App\Http\Controllers\API\V4\PaymentsController;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -71,6 +71,7 @@
'password',
'password_ldap',
'remember_token',
+ 'role'
];
protected $nullable = [
@@ -221,6 +222,10 @@
return false;
}
+ if ($this->role == "admin") {
+ return true;
+ }
+
$wallet = $object->wallet();
// TODO: For now controller can delete/update the account owner,
@@ -242,6 +247,10 @@
return false;
}
+ if ($this->role == "admin") {
+ return true;
+ }
+
if ($object instanceof User && $this->id == $object->id) {
return true;
}
@@ -264,6 +273,10 @@
return false;
}
+ if ($this->role == "admin") {
+ return true;
+ }
+
if ($object instanceof User && $this->id == $object->id) {
return true;
}
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -2,6 +2,9 @@
namespace App;
+use App\Rules\UserEmailLocal;
+use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\Str;
use Ramsey\Uuid\Uuid;
/**
@@ -92,6 +95,69 @@
$countries = include resource_path('countries.php');
$env['countries'] = $countries ?: [];
+ $env['jsapp'] = strpos(request()->getHttpHost(), 'admin.') === 0 ? 'admin.js' : 'user.js';
+
return $env;
}
+
+ /**
+ * Email address (login or alias) validation
+ *
+ * @param string $email Email address
+ * @param \App\User $user The account owner
+ * @param bool $is_alias The email is an alias
+ *
+ * @return string Error message on validation error
+ */
+ public static function validateEmail(
+ string $email,
+ \App\User $user,
+ bool $is_alias = false
+ ): ?string {
+ $attribute = $is_alias ? 'alias' : 'email';
+
+ if (strpos($email, '@') === false) {
+ return \trans('validation.entryinvalid', ['attribute' => $attribute]);
+ }
+
+ list($login, $domain) = explode('@', $email);
+
+ // Check if domain exists
+ $domain = Domain::where('namespace', Str::lower($domain))->first();
+
+ if (empty($domain)) {
+ return \trans('validation.domaininvalid');
+ }
+
+ // Validate login part alone
+ $v = Validator::make(
+ [$attribute => $login],
+ [$attribute => ['required', new UserEmailLocal(!$domain->isPublic())]]
+ );
+
+ if ($v->fails()) {
+ return $v->errors()->toArray()[$attribute][0];
+ }
+
+ // Check if it is one of domains available to the user
+ // TODO: We should have a helper that returns "flat" array with domain names
+ // I guess we could use pluck() somehow
+ $domains = array_map(
+ function ($domain) {
+ return $domain->namespace;
+ },
+ $user->domains()
+ );
+
+ if (!in_array($domain->namespace, $domains)) {
+ return \trans('validation.entryexists', ['attribute' => 'domain']);
+ }
+
+ // Check if user with specified address already exists
+ if (User::findByEmail($email)) {
+ return \trans('validation.entryexists', ['attribute' => $attribute]);
+ }
+
+ return null;
+ }
}
diff --git a/src/database/migrations/2020_03_27_134609_user_table_add_role_column.php b/src/database/migrations/2020_03_27_134609_user_table_add_role_column.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2020_03_27_134609_user_table_add_role_column.php
@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+// phpcs:ignore
+class UserTableAddRoleColumn extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table(
+ 'users',
+ function (Blueprint $table) {
+ $table->string('role')->nullable();
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'users',
+ function (Blueprint $table) {
+ $table->dropColumn('role');
+ }
+ );
+ }
+}
diff --git a/src/database/seeds/DomainSeeder.php b/src/database/seeds/DomainSeeder.php
--- a/src/database/seeds/DomainSeeder.php
+++ b/src/database/seeds/DomainSeeder.php
@@ -36,6 +36,16 @@
);
}
+ if (!in_array(\config('app.domain'), $domains)) {
+ Domain::create(
+ [
+ 'namespace' => \config('app.domain'),
+ 'status' => DOMAIN::STATUS_CONFIRMED + Domain::STATUS_ACTIVE,
+ 'type' => Domain::TYPE_PUBLIC
+ ]
+ );
+ }
+
$domains = [
'example.com',
'example.net',
diff --git a/src/database/seeds/UserSeeder.php b/src/database/seeds/UserSeeder.php
--- a/src/database/seeds/UserSeeder.php
+++ b/src/database/seeds/UserSeeder.php
@@ -132,5 +132,18 @@
$john->assignPackage($package_lite, $joe);
factory(User::class, 10)->create();
+
+ $jeroen = User::create(
+ [
+ 'name' => 'Jeroen van Meeuwen',
+ 'email' => 'jeroen@jeroen.jeroen',
+ 'password' => 'jeroen',
+ 'email_verified_at' => now()
+ ]
+ );
+
+ $jeroen->role = "admin";
+
+ $jeroen->save();
}
}
diff --git a/src/public/mix-manifest.json b/src/public/mix-manifest.json
--- a/src/public/mix-manifest.json
+++ b/src/public/mix-manifest.json
@@ -1,4 +1,5 @@
{
- "/js/app.js": "/js/app.js",
+ "/js/admin.js": "/js/admin.js",
+ "/js/user.js": "/js/user.js",
"/css/app.css": "/css/app.css"
}
diff --git a/src/resources/js/admin.js b/src/resources/js/admin.js
new file mode 100644
--- /dev/null
+++ b/src/resources/js/admin.js
@@ -0,0 +1,9 @@
+/**
+ * Application code for the admin UI
+ */
+
+import router from './routes-admin'
+
+window.router = router
+
+require('./app')
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -8,7 +8,6 @@
import AppComponent from '../vue/App'
import MenuComponent from '../vue/Menu'
-import router from './routes'
import store from './store'
import FontAwesomeIcon from './fontawesome'
import VueToastr from '@deveodk/vue-toastr'
@@ -128,7 +127,7 @@
'menu-component': MenuComponent
},
store,
- router,
+ router: window.router,
data() {
return {
isLoading: true
@@ -165,7 +164,7 @@
axios.defaults.headers.common.Authorization = 'Bearer ' + token
if (dashboard !== false) {
- router.push(store.state.afterLogin || { name: 'dashboard' })
+ this.$router.push(store.state.afterLogin || { name: 'dashboard' })
}
store.state.afterLogin = null
@@ -175,7 +174,7 @@
store.commit('logoutUser')
localStorage.setItem('token', '')
delete axios.defaults.headers.common.Authorization
- router.push({ name: 'login' })
+ this.$router.push({ name: 'login' })
},
// Display "loading" overlay (to be used by route components)
startLoading() {
diff --git a/src/resources/js/routes-admin.js b/src/resources/js/routes-admin.js
new file mode 100644
--- /dev/null
+++ b/src/resources/js/routes-admin.js
@@ -0,0 +1,67 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+Vue.use(VueRouter)
+
+import DashboardComponent from '../vue/Admin/Dashboard'
+import Error404Component from '../vue/404'
+import LoginComponent from '../vue/Login'
+import LogoutComponent from '../vue/Logout'
+import PasswordResetComponent from '../vue/PasswordReset'
+
+import store from './store'
+
+const routes = [
+ {
+ path: '/',
+ redirect: { name: 'dashboard' }
+ },
+ {
+ path: '/dashboard',
+ name: 'dashboard',
+ component: DashboardComponent,
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/login',
+ name: 'login',
+ component: LoginComponent
+ },
+ {
+ path: '/logout',
+ name: 'logout',
+ component: LogoutComponent
+ },
+ {
+ path: '/password-reset/:code?',
+ name: 'password-reset',
+ component: PasswordResetComponent
+ },
+ {
+ name: '404',
+ path: '*',
+ component: Error404Component
+ }
+]
+
+const router = new VueRouter({
+ mode: 'history',
+ routes
+})
+
+router.beforeEach((to, from, next) => {
+ // check if the route requires authentication and user is not logged in
+ if (to.matched.some(route => route.meta.requiresAuth) && !store.state.isLoggedIn) {
+ // remember the original request, to use after login
+ store.state.afterLogin = to;
+
+ // redirect to login page
+ next({ name: 'login' })
+
+ return
+ }
+
+ next()
+})
+
+export default router
diff --git a/src/resources/js/routes.js b/src/resources/js/routes-user.js
rename from src/resources/js/routes.js
rename to src/resources/js/routes-user.js
diff --git a/src/resources/js/user.js b/src/resources/js/user.js
new file mode 100644
--- /dev/null
+++ b/src/resources/js/user.js
@@ -0,0 +1,9 @@
+/**
+ * Application code for the user UI
+ */
+
+import router from './routes-user'
+
+window.router = router
+
+require('./app')
diff --git a/src/resources/views/layouts/app.blade.php b/src/resources/views/layouts/app.blade.php
--- a/src/resources/views/layouts/app.blade.php
+++ b/src/resources/views/layouts/app.blade.php
@@ -17,6 +17,6 @@
</div>
<script>window.config = {!! json_encode($env) !!}</script>
- <script src="{{ asset('js/app.js') }}" defer></script>
+ <script src="{{ asset('js/' . $env['jsapp']) }}" defer></script>
</body>
</html>
diff --git a/src/resources/vue/Admin/Dashboard.vue b/src/resources/vue/Admin/Dashboard.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Admin/Dashboard.vue
@@ -0,0 +1,32 @@
+<template>
+ <div v-if="!$root.isLoading" class="container" dusk="dashboard-component">
+ <div id="dashboard-nav"></div>
+ </div>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ isReady: true
+ }
+ },
+ mounted() {
+ const authInfo = this.$store.state.isLoggedIn ? this.$store.state.authInfo : null
+
+ if (authInfo) {
+
+ } else {
+ this.$root.startLoading()
+ axios.get('/api/auth/info')
+ .then(response => {
+ this.$store.state.authInfo = response.data
+ this.$root.stopLoading()
+ })
+ .catch(this.$root.errorHandler)
+ }
+ },
+ methods: {
+ }
+ }
+</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -19,11 +19,26 @@
'prefix' => 'auth'
],
function ($router) {
- Route::get('info', 'API\UsersController@info');
- Route::post('login', 'API\UsersController@login');
- Route::post('logout', 'API\UsersController@logout');
- Route::post('refresh', 'API\UsersController@refresh');
+ Route::post('login', 'API\AuthController@login');
+ Route::group(
+ ['middleware' => 'auth:api'],
+ function ($router) {
+ Route::get('info', 'API\AuthController@info');
+ Route::post('logout', 'API\AuthController@logout');
+ Route::post('refresh', 'API\AuthController@refresh');
+ }
+ );
+ }
+);
+
+Route::group(
+ [
+ 'domain' => \config('app.domain'),
+ 'middleware' => 'api',
+ 'prefix' => 'auth'
+ ],
+ function ($router) {
Route::post('password-reset/init', 'API\PasswordResetController@init');
Route::post('password-reset/verify', 'API\PasswordResetController@verify');
Route::post('password-reset', 'API\PasswordResetController@reset');
@@ -37,21 +52,47 @@
Route::group(
[
+ 'domain' => \config('app.domain'),
'middleware' => 'auth:api',
'prefix' => 'v4'
],
function () {
- Route::apiResource('domains', API\DomainsController::class);
- Route::get('domains/{id}/confirm', 'API\DomainsController@confirm');
+ Route::apiResource('domains', API\V4\DomainsController::class);
+ Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
- Route::apiResource('entitlements', API\EntitlementsController::class);
- Route::apiResource('packages', API\PackagesController::class);
- Route::apiResource('skus', API\SkusController::class);
- Route::apiResource('users', API\UsersController::class);
- Route::apiResource('wallets', API\WalletsController::class);
+ Route::apiResource('entitlements', API\V4\EntitlementsController::class);
+ Route::apiResource('packages', API\V4\PackagesController::class);
+ Route::apiResource('skus', API\V4\SkusController::class);
+ Route::apiResource('users', API\V4\UsersController::class);
+ Route::apiResource('wallets', API\V4\WalletsController::class);
- Route::post('payments', 'API\PaymentsController@store');
+ Route::post('payments', 'API\V4\PaymentsController@store');
}
);
-Route::post('webhooks/payment/mollie', 'API\PaymentsController@webhook');
+Route::group(
+ [
+ 'domain' => \config('app.domain'),
+ ],
+ function () {
+ Route::post('webhooks/payment/mollie', 'API\V4\PaymentsController@webhook');
+ }
+);
+
+Route::group(
+ [
+ 'domain' => 'admin.' . \config('app.domain'),
+ 'middleware' => ['auth:api', 'admin'],
+ 'prefix' => 'v4',
+ ],
+ function () {
+ Route::apiResource('domains', API\V4\Admin\DomainsController::class);
+ Route::get('domains/{id}/confirm', 'API\V4\Admin\DomainsController@confirm');
+
+ Route::apiResource('entitlements', API\V4\Admin\EntitlementsController::class);
+ Route::apiResource('packages', API\V4\Admin\PackagesController::class);
+ Route::apiResource('skus', API\V4\Admin\SkusController::class);
+ Route::apiResource('users', API\V4\Admin\UsersController::class);
+ Route::apiResource('wallets', API\V4\Admin\WalletsController::class);
+ }
+);
diff --git a/src/tests/Browser/Admin/LogonTest.php b/src/tests/Browser/Admin/LogonTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Admin/LogonTest.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace Tests\Browser\Admin;
+
+use Tests\Browser;
+use Tests\Browser\Components\Menu;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\TestCaseDusk;
+use Illuminate\Foundation\Testing\DatabaseMigrations;
+
+class LogonTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ // This will set baseURL for all tests in this file
+ // If we wanted to visit both user and admin in one test
+ // we can also just call visit() with full url
+ Browser::$baseUrl = str_replace('//', '//admin.', \config('app.url'));
+ }
+
+ /**
+ * Test menu on logon page
+ */
+ public function testLogonMenu(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Home());
+ $browser->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
+ });
+ });
+ }
+
+ /**
+ * Test redirect to /login if user is unauthenticated
+ */
+ public function testLogonRedirect(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/dashboard');
+
+ // Checks if we're really on the login page
+ $browser->waitForLocation('/login')
+ ->on(new Home());
+ });
+ }
+
+ /**
+ * Logon with wrong password/user test
+ */
+ public function testLogonWrongCredentials(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Home())
+ ->submitLogon('jeroen@jeroen.jeroen', 'wrong');
+
+ // Error message
+ $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
+ $browser->assertToastTitle('Error')
+ ->assertToastMessage('Invalid username or password.')
+ ->closeToast();
+ });
+
+ // Checks if we're still on the logon page
+ $browser->on(new Home());
+ });
+ }
+
+ /**
+ * Successful logon test
+ */
+ public function testLogonSuccessful(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Home())
+ ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true);
+
+ // Checks if we're really on Dashboard page
+ $browser->on(new Dashboard())
+ ->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']);
+ })
+ ->assertUser('jeroen@jeroen.jeroen');
+
+ // Test that visiting '/' with logged in user does not open logon form
+ // but "redirects" to the dashboard
+ $browser->visit('/')->on(new Dashboard());
+ });
+ }
+
+ /**
+ * Logout test
+ *
+ * @depends testLogonSuccessful
+ */
+ public function testLogout(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->on(new Dashboard());
+
+ // Click the Logout button
+ $browser->within(new Menu(), function ($browser) {
+ $browser->click('.link-logout');
+ });
+
+ // We expect the logon page
+ $browser->waitForLocation('/login')
+ ->on(new Home());
+
+ // with default menu
+ $browser->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
+ });
+
+ // Success toast message
+ $browser->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
+ $browser->assertToastTitle('')
+ ->assertToastMessage('Successfully logged out')
+ ->closeToast();
+ });
+ });
+ }
+
+ /**
+ * Logout by URL test
+ */
+ public function testLogoutByURL(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Home())
+ ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true);
+
+ // Checks if we're really on Dashboard page
+ $browser->on(new Dashboard());
+
+ // Use /logout url, and expect the logon page
+ $browser->visit('/logout')
+ ->waitForLocation('/login')
+ ->on(new Home());
+
+ // with default menu
+ $browser->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
+ });
+
+ // Success toast message
+ $browser->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
+ $browser->assertToastTitle('')
+ ->assertToastMessage('Successfully logged out')
+ ->closeToast();
+ });
+ });
+ }
+}
diff --git a/src/tests/Browser/Pages/Dashboard.php b/src/tests/Browser/Pages/Dashboard.php
--- a/src/tests/Browser/Pages/Dashboard.php
+++ b/src/tests/Browser/Pages/Dashboard.php
@@ -27,7 +27,7 @@
{
$browser->assertPathIs('/dashboard')
->waitUntilMissing('@app .app-loader')
- ->assertVisible('@links');
+ ->assertPresent('@links');
}
/**
diff --git a/src/tests/Browser/StatusTest.php b/src/tests/Browser/StatusTest.php
--- a/src/tests/Browser/StatusTest.php
+++ b/src/tests/Browser/StatusTest.php
@@ -150,9 +150,9 @@
// Assert user status icons
->assertVisible('@table tbody tr:first-child td:first-child svg.fa-user.text-success')
->assertText('@table tbody tr:first-child td:first-child svg title', 'Active')
- ->assertVisible('@table tbody tr:nth-child(2) td:first-child svg.fa-user.text-danger')
- ->assertText('@table tbody tr:nth-child(2) td:first-child svg title', 'Not Ready')
- ->click('@table tbody tr:nth-child(2) td:first-child a')
+ ->assertVisible('@table tbody tr:nth-child(3) td:first-child svg.fa-user.text-danger')
+ ->assertText('@table tbody tr:nth-child(3) td:first-child svg title', 'Not Ready')
+ ->click('@table tbody tr:nth-child(3) td:first-child a')
->on(new UserInfo())
->with('@form', function (Browser $browser) {
// Assert stet in the user edit form
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -108,11 +108,13 @@
->whenAvailable('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 4)
->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(2) a', 'john@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(3) a', 'ned@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org')
->assertVisible('tbody tr:nth-child(1) button.button-delete')
->assertVisible('tbody tr:nth-child(2) button.button-delete')
- ->assertVisible('tbody tr:nth-child(3) button.button-delete');
+ ->assertVisible('tbody tr:nth-child(3) button.button-delete')
+ ->assertVisible('tbody tr:nth-child(4) button.button-delete');
});
});
}
@@ -126,7 +128,7 @@
{
$this->browse(function (Browser $browser) {
$browser->on(new UserList())
- ->click('@table tr:nth-child(2) a')
+ ->click('@table tr:nth-child(3) a')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
->with('@form', function (Browser $browser) {
@@ -427,8 +429,8 @@
->waitForLocation('/users')
->on(new UserList())
->whenAvailable('@table', function (Browser $browser) {
- $browser->assertElementsCount('tbody tr', 4)
- ->assertSeeIn('tbody tr:nth-child(3) a', 'julia.roberts@kolab.org');
+ $browser->assertElementsCount('tbody tr', 5)
+ ->assertSeeIn('tbody tr:nth-child(4) a', 'julia.roberts@kolab.org');
});
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
@@ -455,9 +457,9 @@
$this->browse(function (Browser $browser) {
$browser->visit(new UserList())
->whenAvailable('@table', function (Browser $browser) {
- $browser->assertElementsCount('tbody tr', 4)
- ->assertSeeIn('tbody tr:nth-child(3) a', 'julia.roberts@kolab.org')
- ->click('tbody tr:nth-child(3) button.button-delete');
+ $browser->assertElementsCount('tbody tr', 5)
+ ->assertSeeIn('tbody tr:nth-child(4) a', 'julia.roberts@kolab.org')
+ ->click('tbody tr:nth-child(4) button.button-delete');
})
->with(new Dialog('#delete-warning'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Delete julia.roberts@kolab.org')
@@ -467,7 +469,7 @@
->click('@button-cancel');
})
->whenAvailable('@table', function (Browser $browser) {
- $browser->click('tbody tr:nth-child(3) button.button-delete');
+ $browser->click('tbody tr:nth-child(4) button.button-delete');
})
->with(new Dialog('#delete-warning'), function (Browser $browser) {
$browser->click('@button-action');
@@ -478,10 +480,11 @@
->closeToast();
})
->with('@table', function (Browser $browser) {
- $browser->assertElementsCount('tbody tr', 3)
+ $browser->assertElementsCount('tbody tr', 4)
->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(2) a', 'john@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(3) a', 'ned@kolab.org');
+ ->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org');
});
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
@@ -490,7 +493,7 @@
// Test clicking Delete on the controller record redirects to /profile/delete
$browser
->with('@table', function (Browser $browser) {
- $browser->click('tbody tr:nth-child(2) button.button-delete');
+ $browser->click('tbody tr:nth-child(3) button.button-delete');
})
->waitForLocation('/profile/delete');
});
@@ -514,8 +517,8 @@
->submitLogon('ned@kolab.org', 'simple123', true)
->visit(new UserList())
->whenAvailable('@table', function (Browser $browser) {
- $browser->assertElementsCount('tbody tr', 3)
- ->assertElementsCount('tbody button.button-delete', 3);
+ $browser->assertElementsCount('tbody tr', 4)
+ ->assertElementsCount('tbody button.button-delete', 4);
});
// TODO: Test the delete action in details
diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/Admin/UsersTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Tests\Feature\Controller\Admin;
+
+use App\Domain;
+use App\User;
+use Tests\TestCase;
+
+class UsersTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ // This will set base URL for all tests in this file
+ // If we wanted to access both user and admin in one test
+ // we can also just call post/get/whatever with full url
+ \config(['app.url' => str_replace('//', '//admin.', \config('app.url'))]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ /**
+ * Test (/api/v4/index)
+ */
+ public function testIndex(): void
+ {
+ $user = $this->getTestUser('john@kolab.org');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+
+ $response = $this->actingAs($user)->get("api/v4/users");
+ $response->assertStatus(403);
+
+ $response = $this->actingAs($admin)->get("api/v4/users");
+ $response->assertStatus(200);
+
+ // TODO: Test the response
+ $this->markTestIncomplete();
+ }
+}
diff --git a/src/tests/Feature/Controller/AuthTest.php b/src/tests/Feature/Controller/AuthTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/AuthTest.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Domain;
+use App\User;
+use Tests\TestCase;
+
+class AuthTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('UsersControllerTest1@userscontroller.com');
+ $this->deleteTestDomain('userscontroller.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('UsersControllerTest1@userscontroller.com');
+ $this->deleteTestDomain('userscontroller.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test fetching current user info (/api/auth/info)
+ */
+ public function testInfo(): void
+ {
+ $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $domain = $this->getTestDomain('userscontroller.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_PUBLIC,
+ ]);
+
+ $response = $this->actingAs($user)->get("api/auth/info");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertEquals($user->id, $json['id']);
+ $this->assertEquals($user->email, $json['email']);
+ $this->assertEquals(User::STATUS_NEW | User::STATUS_ACTIVE, $json['status']);
+ $this->assertTrue(is_array($json['statusInfo']));
+ $this->assertTrue(is_array($json['settings']));
+ $this->assertTrue(is_array($json['aliases']));
+
+ // Note: Details of the content are tested in testUserResponse()
+ }
+
+ /**
+ * Test /api/auth/login
+ */
+ public function testLogin(): string
+ {
+ // Request with no data
+ $response = $this->post("api/auth/login", []);
+ $response->assertStatus(422);
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json['errors']);
+ $this->assertArrayHasKey('email', $json['errors']);
+ $this->assertArrayHasKey('password', $json['errors']);
+
+ // Request with invalid password
+ $post = ['email' => 'john@kolab.org', 'password' => 'wrong'];
+ $response = $this->post("api/auth/login", $post);
+ $response->assertStatus(401);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('Invalid username or password.', $json['message']);
+
+ // Valid user+password
+ $post = ['email' => 'john@kolab.org', 'password' => 'simple123'];
+ $response = $this->post("api/auth/login", $post);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+ $this->assertTrue(!empty($json['access_token']));
+ $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']);
+ $this->assertEquals('bearer', $json['token_type']);
+
+ // TODO: We have browser tests for 2FA but we should probably also test it here
+
+ return $json['access_token'];
+ }
+
+ /**
+ * Test /api/auth/logout
+ *
+ * @depends testLogin
+ */
+ public function testLogout($token): void
+ {
+ // Request with no token, testing that it requires auth
+ $response = $this->post("api/auth/logout");
+ $response->assertStatus(401);
+
+ // Test the same using JSON mode
+ $response = $this->json('POST', "api/auth/logout", []);
+ $response->assertStatus(401);
+
+ // Request with valid token
+ $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/logout");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertEquals('success', $json['status']);
+ $this->assertEquals('Successfully logged out.', $json['message']);
+
+ // Check if it really destroyed the token?
+ $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->get("api/auth/info");
+ $response->assertStatus(401);
+ }
+
+ public function testRefresh(): void
+ {
+ // TODO
+ $this->markTestIncomplete();
+ }
+}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -2,7 +2,7 @@
namespace Tests\Feature\Controller;
-use App\Http\Controllers\API\SkusController;
+use App\Http\Controllers\API\V4\SkusController;
use App\Sku;
use Tests\TestCase;
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -4,7 +4,7 @@
use App\Discount;
use App\Domain;
-use App\Http\Controllers\API\UsersController;
+use App\Http\Controllers\API\V4\UsersController;
use App\Package;
use App\Sku;
use App\User;
@@ -56,32 +56,6 @@
}
/**
- * Test fetching current user info (/api/auth/info)
- */
- public function testInfo(): void
- {
- $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
- $domain = $this->getTestDomain('userscontroller.com', [
- 'status' => Domain::STATUS_NEW,
- 'type' => Domain::TYPE_PUBLIC,
- ]);
-
- $response = $this->actingAs($user)->get("api/auth/info");
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertEquals($user->id, $json['id']);
- $this->assertEquals($user->email, $json['email']);
- $this->assertEquals(User::STATUS_NEW | User::STATUS_ACTIVE, $json['status']);
- $this->assertTrue(is_array($json['statusInfo']));
- $this->assertTrue(is_array($json['settings']));
- $this->assertTrue(is_array($json['aliases']));
-
- // Note: Details of the content are tested in testUserResponse()
- }
-
- /**
* Test user deleting (DELETE /api/v4/users/<id>)
*/
public function testDestroy(): void
@@ -235,81 +209,6 @@
$this->assertSame($ned->email, $json[3]['email']);
}
- /**
- * Test /api/auth/login
- */
- public function testLogin(): string
- {
- // Request with no data
- $response = $this->post("api/auth/login", []);
- $response->assertStatus(422);
- $json = $response->json();
-
- $this->assertSame('error', $json['status']);
- $this->assertCount(2, $json['errors']);
- $this->assertArrayHasKey('email', $json['errors']);
- $this->assertArrayHasKey('password', $json['errors']);
-
- // Request with invalid password
- $post = ['email' => 'john@kolab.org', 'password' => 'wrong'];
- $response = $this->post("api/auth/login", $post);
- $response->assertStatus(401);
-
- $json = $response->json();
-
- $this->assertSame('error', $json['status']);
- $this->assertSame('Invalid username or password.', $json['message']);
-
- // Valid user+password
- $post = ['email' => 'john@kolab.org', 'password' => 'simple123'];
- $response = $this->post("api/auth/login", $post);
- $json = $response->json();
-
- $response->assertStatus(200);
- $this->assertTrue(!empty($json['access_token']));
- $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']);
- $this->assertEquals('bearer', $json['token_type']);
-
- // TODO: We have browser tests for 2FA but we should probably also test it here
-
- return $json['access_token'];
- }
-
- /**
- * Test /api/auth/logout
- *
- * @depends testLogin
- */
- public function testLogout($token): void
- {
- // Request with no token, testing that it requires auth
- $response = $this->post("api/auth/logout");
- $response->assertStatus(401);
-
- // Test the same using JSON mode
- $response = $this->json('POST', "api/auth/logout", []);
- $response->assertStatus(401);
-
- // Request with valid token
- $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/logout");
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertEquals('success', $json['status']);
- $this->assertEquals('Successfully logged out.', $json['message']);
-
- // Check if it really destroyed the token?
- $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->get("api/auth/info");
- $response->assertStatus(401);
- }
-
- public function testRefresh(): void
- {
- // TODO
- $this->markTestIncomplete();
- }
-
public function testStatusInfo(): void
{
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
@@ -485,6 +384,7 @@
$secondfactor_sku = Sku::where('title', '2fa')->first();
$this->assertCount(5, $json['skus']);
+
$this->assertSame(2, $json['skus'][$storage_sku->id]['count']);
$this->assertSame(1, $json['skus'][$groupware_sku->id]['count']);
$this->assertSame(1, $json['skus'][$mailbox_sku->id]['count']);
@@ -866,7 +766,7 @@
*/
public function testValidateEmail($alias, $user, $is_alias, $expected_result): void
{
- $result = $this->invokeMethod(new UsersController(), 'validateEmail', [$alias, $user, $is_alias]);
+ $result = $this->invokeMethod(new \App\Utils(), 'validateEmail', [$alias, $user, $is_alias]);
$this->assertSame($expected_result, $result);
}
diff --git a/src/webpack.mix.js b/src/webpack.mix.js
--- a/src/webpack.mix.js
+++ b/src/webpack.mix.js
@@ -11,5 +11,6 @@
|
*/
-mix.js('resources/js/app.js', 'public/js')
+mix.js('resources/js/user.js', 'public/js')
+ .js('resources/js/admin.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 30, 7:00 AM (6 d, 8 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18795234
Default Alt Text
D1129.1774854057.diff (63 KB)

Event Timeline