Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117523812
D1129.1774854057.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
63 KB
Referenced Files
None
Subscribers
None
D1129.1774854057.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D1129: Split user and admin interfaces
Attached
Detach File
Event Timeline