Page MenuHomePhorge

D5574.1775227340.diff
No OneTemporary

Authored By
Unknown
Size
53 KB
Referenced Files
None
Subscribers
None

D5574.1775227340.diff

diff --git a/src/app/Device.php b/src/app/Device.php
--- a/src/app/Device.php
+++ b/src/app/Device.php
@@ -2,6 +2,7 @@
namespace App;
+use App\Http\Resources\PlanResource;
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
use App\Traits\UuidIntKeyTrait;
@@ -55,11 +56,18 @@
*/
public function info(): array
{
+ $plans = Plan::withObjectTenantContext($this)->where('mode', 'token')
+ ->orderByDesc('months')->orderByDesc('title')
+ ->get();
+
$result = [
+ // Device registration date-time
'created_at' => (string) $this->created_at,
+ // Plans available for signup via a device token
+ 'plans' => PlanResource::collection($plans),
];
- // TODO: Include other information about the wallet/payments state
+ // TODO: Include other information about the plan/wallet/payments state
return $result;
}
diff --git a/src/app/Http/Controllers/API/AuthController.php b/src/app/Http/Controllers/API/AuthController.php
--- a/src/app/Http/Controllers/API/AuthController.php
+++ b/src/app/Http/Controllers/API/AuthController.php
@@ -5,6 +5,8 @@
use App\Auth\OAuth;
use App\AuthAttempt;
use App\Http\Controllers\Controller;
+use App\Http\Resources\AuthResource;
+use App\Http\Resources\UserInfoResource;
use App\User;
use App\Utils;
use Illuminate\Http\JsonResponse;
@@ -19,21 +21,17 @@
class AuthController extends Controller
{
/**
- * Get the authenticated User
+ * User information.
+ *
+ * Note that the same information is by default included in the `auth/login` response.
*
* @return JsonResponse
*/
public function info()
{
- $user = $this->guard()->user();
-
- if (!empty(request()->input('refresh'))) {
- return $this->refreshAndRespond(request(), $user);
- }
-
- $response = V4\UsersController::userResponse($user);
+ $response = new UserInfoResource($this->guard()->user());
- return response()->json($response);
+ return $response->response();
}
/**
@@ -57,6 +55,7 @@
'scope' => 'api',
'secondfactor' => $secondFactor,
]);
+
$proxyRequest->headers->set('X-Client-IP', request()->ip());
$tokenResponse = app()->handle($proxyRequest);
@@ -65,11 +64,15 @@
}
/**
- * Get an oauth token via given credentials.
+ * User logon.
+ *
+ * Returns an authentication token(s) and user information.
*
- * @param Request $request the API request
+ * @param Request $request The API request
*
* @return JsonResponse
+ *
+ * @unauthenticated
*/
public function login(Request $request)
{
@@ -101,7 +104,7 @@
}
/**
- * Approval request for the oauth authorization endpoint
+ * OAuth (SSO) authorization.
*
* * The user is authenticated via the regular login page
* * We assume implicit consent in the Authorization page
@@ -137,16 +140,20 @@
}
/**
- * Get the user (geo) location
+ * User (geo) location
*
* @return JsonResponse
+ *
+ * @unauthenticated
*/
public function location()
{
$ip = request()->ip();
$response = [
+ // Client IP address
'ipAddress' => $ip,
+ // Client country code (derived from the IP address)
'countryCode' => Utils::countryForIP($ip, ''),
];
@@ -154,7 +161,9 @@
}
/**
- * Log the user out (Invalidate the token)
+ * User logout.
+ *
+ * Revokes the authentication token.
*
* @return JsonResponse
*/
@@ -177,25 +186,21 @@
}
/**
- * Refresh a token.
+ * Authentication token refresh.
*
* @return JsonResponse
*/
public function refresh(Request $request)
{
- return self::refreshAndRespond($request);
- }
+ $request->validate([
+ // Request user information in the response
+ 'info' => 'bool',
+ // A refresh token
+ 'refresh_token' => 'string|required',
+ ]);
+
+ $user = $request->info ? $this->guard()->user() : null;
- /**
- * Refresh the token and respond with it.
- *
- * @param Request $request the API request
- * @param ?User $user The user being authenticated
- *
- * @return JsonResponse
- */
- protected static function refreshAndRespond(Request $request, $user = null)
- {
$proxyRequest = Request::create('/oauth/token', 'POST', [
'grant_type' => 'refresh_token',
'refresh_token' => $request->refresh_token,
@@ -214,10 +219,8 @@
* @param Response $tokenResponse the response containing the token
* @param ?User $user The user being authenticated
* @param ?bool $mode Response mode: 'fast' - return minimum set of user data
- *
- * @return JsonResponse
*/
- protected static function respondWithToken($tokenResponse, $user = null, $mode = null)
+ protected static function respondWithToken($tokenResponse, $user = null, $mode = null): JsonResponse
{
$data = json_decode($tokenResponse->getContent());
@@ -244,22 +247,16 @@
return response()->json($response, 401);
}
- $response = [];
+ $response = new AuthResource($data);
if ($user) {
if ($mode == 'fast') {
- $response['id'] = $user->id;
+ $response->user_id = $user->id;
} else {
- $response = V4\UsersController::userResponse($user);
+ $response->withUserInfo(new UserInfoResource($user));
}
}
- $response['status'] = 'success';
- $response['access_token'] = $data->access_token;
- $response['refresh_token'] = $data->refresh_token;
- $response['token_type'] = 'bearer';
- $response['expires_in'] = $data->expires_in;
-
- return response()->json($response);
+ return $response->response();
}
}
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
@@ -26,6 +26,8 @@
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
public function init(Request $request)
{
@@ -72,6 +74,8 @@
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
public function verify(Request $request)
{
@@ -118,6 +122,8 @@
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
public function reset(Request $request)
{
@@ -147,6 +153,8 @@
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
public function resetExpired(Request $request)
{
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
@@ -6,6 +6,7 @@
use App\Domain;
use App\Group;
use App\Http\Controllers\Controller;
+use App\Http\Resources\PlanResource;
use App\Jobs\Mail\SignupVerificationJob;
use App\Payment;
use App\Plan;
@@ -24,6 +25,7 @@
use App\User;
use App\Utils;
use App\VatRate;
+use Dedoc\Scramble\Attributes\BodyParameter;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -36,11 +38,13 @@
class SignupController extends Controller
{
/**
- * Returns plans definitions for signup.
+ * List of plans for signup.
*
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
public function plans(Request $request)
{
@@ -48,49 +52,51 @@
// But prefer monthly on left, yearly on right
$plans = Plan::withEnvTenantContext()->where('hidden', false)
->orderBy('months')->orderByDesc('title')
- ->get()
- ->map(static function ($plan) {
- $button = self::trans("app.planbutton-{$plan->title}");
- if (str_contains($button, 'app.planbutton')) {
- $button = self::trans('app.planbutton', ['plan' => $plan->name]);
- }
-
- return [
- 'title' => $plan->title,
- 'name' => $plan->name,
- 'button' => $button,
- 'description' => $plan->description,
- 'mode' => $plan->mode ?: Plan::MODE_EMAIL,
- 'isDomain' => $plan->hasDomain(),
- ];
- })
- ->all();
+ ->get();
- return response()->json(['status' => 'success', 'plans' => $plans]);
+ return response()->json([
+ 'status' => 'success',
+ // List of plans available for signup
+ 'plans' => PlanResource::collection($plans),
+ ]);
}
/**
- * Returns list of public domains for signup.
+ * List of public domains for signup.
*
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
public function domains(Request $request)
{
- return response()->json(['status' => 'success', 'domains' => Domain::getPublicDomains()]);
+ return response()->json([
+ 'status' => 'success',
+ // @var array<string> List of domain namespaces available for signup
+ 'domains' => Domain::getPublicDomains(),
+ ]);
}
/**
* Starts signup process.
*
- * Verifies user name and email/phone, sends verification email/sms message.
- * Returns the verification code.
+ * Verifies user name and email, sends verification message. Returns the verification code.
*
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
+ #[BodyParameter('plan', description: 'Plan identifier', type: 'string', required: true)]
+ #[BodyParameter('first_name', description: 'First name', type: 'string')]
+ #[BodyParameter('last_name', description: 'Last name', type: 'string')]
+ #[BodyParameter('email', description: 'External email address for verification (required for non-token signup)', type: 'string')]
+ #[BodyParameter('referral', description: 'Referral program code', type: 'string')]
+ #[BodyParameter('voucher', description: 'Voucher code', type: 'string')]
+ #[BodyParameter('token', description: 'Signup token (required for token-mode signup)', type: 'string')]
public function init(Request $request)
{
// Don't allow URLs in user names preventing abuse of signup email
@@ -136,10 +142,16 @@
$response = [
'status' => 'success',
+ // Verification code
'code' => $code->code,
+ // Plan mode
'mode' => $plan->mode ?: 'email',
+ // List of domain namespaces available for signup
'domains' => Domain::getPublicDomains(),
+ // @var bool Indicates that the plan is viable for a custom domain signup
'is_domain' => $plan->hasDomain(),
+ // @var string|null Short verification code (for token signups only)
+ 'short_code' => null,
];
if ($plan->mode == Plan::MODE_TOKEN) {
@@ -148,17 +160,20 @@
} else {
// External email verification, send an email message
SignupVerificationJob::dispatch($code);
+ unset($response['short_code']);
}
return response()->json($response);
}
/**
- * Returns signup invitation information.
+ * Signup invitation information.
*
* @param string $id Signup invitation identifier
*
* @return JsonResponse|void
+ *
+ * @unauthenticated
*/
public function invitation($id)
{
@@ -168,9 +183,10 @@
return $this->errorResponse(404);
}
- $result = ['id' => $id];
-
- return response()->json($result);
+ return response()->json([
+ // Signup invitation identifier
+ 'id' => $id,
+ ]);
}
/**
@@ -180,7 +196,11 @@
* @param bool $update Update the signup code record
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
+ #[BodyParameter('code', description: 'Verification code', type: 'string', required: true)]
+ #[BodyParameter('short_code', description: 'Short code', type: 'string', required: true)]
public function verify(Request $request, $update = true)
{
// Validate the request args
@@ -224,14 +244,17 @@
$code->save();
}
- // Return user name and email/phone/voucher from the codes database,
- // domains list for selection and "plan type" flag
return response()->json([
'status' => 'success',
+ // User email address to sign up for
'email' => $code->email,
+ // First name
'first_name' => $code->first_name,
+ // Last name
'last_name' => $code->last_name,
+ // Voucher code
'voucher' => $code->voucher,
+ // @var bool Indicates that the plan is viable for a custom domain signup
'is_domain' => $plan->hasDomain(),
]);
}
@@ -242,7 +265,18 @@
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
+ #[BodyParameter('login', description: 'User login', type: 'string', required: true)]
+ #[BodyParameter('domain', description: 'User domain namespace', type: 'string', required: true)]
+ #[BodyParameter('password', description: 'User password', type: 'string', required: true)]
+ #[BodyParameter('voucher', description: 'Voucher code', type: 'string')]
+ #[BodyParameter('plan', description: 'Plan identifier (required when not using a verification code)', type: 'string')]
+ #[BodyParameter('invitation', description: 'Signup invitation identifier', type: 'string')]
+ #[BodyParameter('token', description: 'Signup token (required for mode=token plans)', type: 'string')]
+ #[BodyParameter('first_name', description: 'First name', type: 'string')]
+ #[BodyParameter('last_name', description: 'Last name', type: 'string')]
public function signupValidate(Request $request)
{
$rules = [
@@ -353,11 +387,15 @@
}
/**
- * Finishes the signup process by creating the user account.
+ * Finishes the signup process.
+ *
+ * On success creates a new account and returns authentication token(s) and user information.
*
* @param Request $request HTTP request
*
* @return JsonResponse JSON response
+ *
+ * @unauthenticated
*/
public function signup(Request $request)
{
@@ -455,6 +493,7 @@
if ($request->plan->mode == Plan::MODE_MANDATE) {
$data = $response->getData(true);
+ // TODO: Make it visible in the API Doc
$data['checkout'] = $this->mandateForPlan($request->plan, $request->discount, $user);
$response->setData($data);
}
diff --git a/src/app/Http/Controllers/API/V4/DeviceController.php b/src/app/Http/Controllers/API/V4/DeviceController.php
--- a/src/app/Http/Controllers/API/V4/DeviceController.php
+++ b/src/app/Http/Controllers/API/V4/DeviceController.php
@@ -15,6 +15,8 @@
* @param string $hash Device secret identifier
*
* @return JsonResponse The response
+ *
+ * @unauthenticated
*/
public function claim(string $hash)
{
@@ -42,6 +44,8 @@
* @param string $hash Device secret identifier
*
* @return JsonResponse The response
+ *
+ * @unauthenticated
*/
public function info(string $hash)
{
diff --git a/src/app/Http/Controllers/API/V4/PolicyController.php b/src/app/Http/Controllers/API/V4/PolicyController.php
--- a/src/app/Http/Controllers/API/V4/PolicyController.php
+++ b/src/app/Http/Controllers/API/V4/PolicyController.php
@@ -10,6 +10,7 @@
use App\Policy\SmtpAccess;
use App\Policy\SPF;
use App\User;
+use Dedoc\Scramble\Attributes\BodyParameter;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -20,7 +21,11 @@
* Validate the password regarding the defined policies.
*
* @return JsonResponse
+ *
+ * @unauthenticated
*/
+ #[BodyParameter('user', description: 'User identifier', type: 'string')]
+ #[BodyParameter('password', description: 'User password', type: 'string', required: true)]
public function checkPassword(Request $request)
{
$userId = $request->input('user');
@@ -37,8 +42,11 @@
);
return response()->json([
+ // Policy check status
'status' => count($passed) == count($status) ? 'success' : 'error',
+ // @var array Policy check result by rule
'list' => array_values($status),
+ // @var int Number of rules in the result list
'count' => count($status),
]);
}
diff --git a/src/app/Http/Controllers/API/V4/SupportController.php b/src/app/Http/Controllers/API/V4/SupportController.php
--- a/src/app/Http/Controllers/API/V4/SupportController.php
+++ b/src/app/Http/Controllers/API/V4/SupportController.php
@@ -14,26 +14,27 @@
* Submit contact request form.
*
* @return JsonResponse
+ *
+ * @unauthenticated
*/
public function request(Request $request)
{
- $rules = [
+ // Check required fields
+ $request->validate($rules = [
+ // User identifier
'user' => 'string|nullable|max:256',
+ // User name
'name' => 'string|nullable|max:256',
+ // Contact email address
'email' => 'required|email',
+ // Request summary
'summary' => 'required|string|max:512',
+ // Request body
'body' => 'required|string',
- ];
+ ]);
$params = $request->only(array_keys($rules));
- // Check required fields
- $v = Validator::make($params, $rules);
-
- if ($v->fails()) {
- return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
- }
-
$to = \config('app.support_email');
if (empty($to)) {
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -4,15 +4,13 @@
use App\Auth\OAuth;
use App\Domain;
-use App\Entitlement;
use App\Group;
use App\Http\Controllers\API\V4\User\DelegationTrait;
use App\Http\Controllers\RelationController;
+use App\Http\Resources\UserInfoFullResource;
use App\Jobs\User\CreateJob;
use App\License;
use App\Package;
-use App\Plan;
-use App\Providers\PaymentProvider;
use App\Resource;
use App\Rules\Password;
use App\Rules\UserEmailLocal;
@@ -20,7 +18,6 @@
use App\Sku;
use App\User;
use App\VerificationCode;
-use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -33,18 +30,6 @@
{
use DelegationTrait;
- /** @const array List of user setting keys available for modification in UI */
- public const USER_SETTINGS = [
- 'billing_address',
- 'country',
- 'currency',
- 'external_email',
- 'first_name',
- 'last_name',
- 'organization',
- 'phone',
- ];
-
/**
* On user create it is filled with a user or group object to force-delete
* before the creation of a new user record is possible.
@@ -119,6 +104,7 @@
$result = [
'list' => $result,
+ // @var int
'count' => count($result),
'hasMore' => $hasMore,
];
@@ -218,9 +204,9 @@
}
/**
- * Display information on the user account specified by $id.
+ * User information.
*
- * @param string $id the account to show information for
+ * @param string $id The user identifier
*
* @return JsonResponse
*/
@@ -236,22 +222,9 @@
return $this->errorResponse(403);
}
- $response = $this->userResponse($user);
-
- $response['skus'] = Entitlement::objectEntitlementsSummary($user);
- $response['config'] = $user->getConfig(true);
- $response['aliases'] = $user->aliases()->pluck('alias')->all();
- $response['canDelete'] = $this->guard()->user()->canDelete($user);
+ $response = new UserInfoFullResource($user);
- $code = $user->verificationcodes()->where('active', true)
- ->where('expires_at', '>', Carbon::now())
- ->first();
-
- if ($code) {
- $response['passwordLinkCode'] = $code->short_code . '-' . $code->code;
- }
-
- return response()->json($response);
+ return $response->response();
}
/**
@@ -444,56 +417,6 @@
return response()->json($response);
}
- /**
- * Create a response data array for specified user.
- *
- * @param User $user User object
- *
- * @return array Response data
- */
- public static function userResponse(User $user): array
- {
- $response = array_merge($user->toArray(), self::objectState($user));
-
- $wallet = $user->wallet();
-
- // IsLocked flag to lock the user to the Wallet page only
- $response['isLocked'] = !$user->isActive() && $wallet->plan()?->mode == Plan::MODE_MANDATE;
-
- // Settings
- $keys = array_merge(self::USER_SETTINGS, ['password_expired', 'debug']);
- $response['settings'] = $user->settings()->whereIn('key', $keys)->pluck('value', 'key')->all();
-
- // Status info
- $response['statusInfo'] = self::statusInfo($user);
-
- // Add more info to the wallet object output
- $map_func = static function ($wallet) use ($user) {
- $result = $wallet->toArray();
-
- if ($wallet->discount) {
- $result['discount'] = $wallet->discount->discount;
- $result['discount_description'] = $wallet->discount->description;
- }
-
- if ($wallet->user_id != $user->id) {
- $result['user_email'] = $wallet->owner->email;
- }
-
- $provider = PaymentProvider::factory($wallet);
- $result['provider'] = $provider->name();
-
- return $result;
- };
-
- // Information about wallets and accounts for access checks
- $response['wallets'] = $user->wallets->map($map_func)->toArray();
- $response['accounts'] = $user->accounts->map($map_func)->toArray();
- $response['wallet'] = $map_func($wallet);
-
- return $response;
- }
-
/**
* Prepare user statuses for the UI
*
@@ -501,7 +424,7 @@
*
* @return array Statuses array
*/
- protected static function objectState($user): array
+ public static function objectState($user): array
{
$state = parent::objectState($user);
diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php
--- a/src/app/Http/Controllers/Controller.php
+++ b/src/app/Http/Controllers/Controller.php
@@ -41,6 +41,7 @@
$response = [
'status' => 'error',
+ // @var string
'message' => $message ?: ($errors[$code] ?? "Server error"),
];
diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php
--- a/src/app/Http/Controllers/RelationController.php
+++ b/src/app/Http/Controllers/RelationController.php
@@ -121,7 +121,7 @@
*
* @return array Statuses array
*/
- protected static function objectState($resource): array
+ public static function objectState($resource): array
{
$state = [];
diff --git a/src/app/Http/Resources/AuthResource.php b/src/app/Http/Resources/AuthResource.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Resources/AuthResource.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+/**
+ * Authentication response
+ */
+class AuthResource extends JsonResource
+{
+ public string $status = 'success';
+ public ?int $user_id = null;
+
+ private ?UserInfoResource $userinfo = null;
+
+ /**
+ * Add user information to the response
+ */
+ public function withUserInfo(UserInfoResource $userinfo): void
+ {
+ $this->userinfo = $userinfo;
+ $this->user_id = $userinfo->id;
+ }
+
+ /**
+ * Transform the resource into an array.
+ */
+ public function toArray(Request $request): array
+ {
+ $extra = $this->userinfo ? $this->userinfo->toArray($request) : [];
+
+ return [
+ // Authentication token
+ 'access_token' => $this->resource->access_token,
+ // Refresh token
+ 'refresh_token' => $this->resource->refresh_token,
+ // Token type
+ 'token_type' => \strtolower($this->resource->token_type),
+ // Token expiration time (in seconds)
+ 'expires_in' => (int) $this->resource->expires_in,
+ // Response status
+ 'status' => $this->status,
+ // @var int User identifier
+ 'id' => $this->when(isset($this->user_id), $this->user_id),
+ // @var UserInfoResource User information
+ 'user' => $this->when(isset($this->userinfo), $this->userinfo),
+ ];
+ }
+}
diff --git a/src/app/Http/Resources/PlanResource.php b/src/app/Http/Resources/PlanResource.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Resources/PlanResource.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Http\Resources;
+
+use App\Http\Controllers\Controller;
+use App\Plan;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+/**
+ * Plan response
+ *
+ * @mixin Plan
+ */
+class PlanResource extends JsonResource
+{
+ /**
+ * Transform the resource into an array.
+ */
+ public function toArray(Request $request): array
+ {
+ $button = Controller::trans("app.planbutton-{$this->resource->title}");
+ if (str_contains($button, 'app.planbutton')) {
+ $button = Controller::trans('app.planbutton', ['plan' => $this->resource->name]);
+ }
+
+ return [
+ // @var string Plan title (identifier)
+ 'title' => $this->resource->title,
+ // @var string Plan name
+ 'name' => $this->resource->name,
+ // @var string Plan description
+ 'description' => $this->resource->description,
+ // @var string Button label
+ 'button' => $button,
+ // @var string Plan mode (email, token, mandate, etc.)
+ 'mode' => $this->resource->mode ?: Plan::MODE_EMAIL,
+ // @var bool Is the plan viable for a custom domain?
+ 'isDomain' => $this->resource->hasDomain(),
+ // @var int Minimum number of months this plan is for
+ 'months' => $this->resource->months,
+ // @var int Cumulative cost (in cents)
+ 'cost' => $this->resource->cost(),
+ ];
+ }
+}
diff --git a/src/app/Http/Resources/UserInfoFullResource.php b/src/app/Http/Resources/UserInfoFullResource.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Resources/UserInfoFullResource.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Http\Resources;
+
+use App\Entitlement;
+use App\User;
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+/**
+ * Full user information response
+ *
+ * @mixin User
+ */
+class UserInfoFullResource extends UserInfoResource
+{
+ /**
+ * Transform the resource into an array.
+ */
+ public function toArray(Request $request): array
+ {
+ $code = $this->resource->verificationcodes()->where('active', true)
+ ->where('expires_at', '>', Carbon::now())
+ ->first();
+
+ return [
+ $this->merge(parent::toArray($request)),
+ // @var array User subscriptions summary
+ 'skus' => Entitlement::objectEntitlementsSummary($this->resource),
+ // @var array User configuration
+ 'config' => $this->resource->getConfig(true),
+ // @var array<string> Email address aliases
+ 'aliases' => $this->resource->aliases()->pluck('alias')->all(),
+ // @var string Password reset link code
+ 'passwordLinkCode' => $this->when(isset($code), $code ? ($code->short_code . '-' . $code->code) : null),
+ // @var bool Authenticated user permission to delete the user
+ 'canDelete' => Auth::guard()->user()->canDelete($this->resource),
+ ];
+ }
+}
diff --git a/src/app/Http/Resources/UserInfoResource.php b/src/app/Http/Resources/UserInfoResource.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Resources/UserInfoResource.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace App\Http\Resources;
+
+use App\Http\Controllers\API\V4\UsersController;
+use App\Plan;
+use App\Providers\PaymentProvider;
+use App\User;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+/**
+ * User information response
+ *
+ * @mixin User
+ */
+class UserInfoResource extends JsonResource
+{
+ /** @const array List of user setting keys available for modification in UI */
+ public const USER_SETTINGS = [
+ 'billing_address',
+ 'country',
+ 'currency',
+ 'external_email',
+ 'first_name',
+ 'last_name',
+ 'organization',
+ 'phone',
+ ];
+
+ /**
+ * Transform the resource into an array.
+ */
+ public function toArray(Request $request): array
+ {
+ $wallet = $this->resource->wallet();
+
+ // IsLocked flag to lock the user to the Wallet page only
+ $isLocked = !$this->resource->isActive() && $wallet->plan()?->mode == Plan::MODE_MANDATE;
+
+ // Settings
+ $keys = array_merge(self::USER_SETTINGS, ['password_expired', 'debug']);
+ $settings = $this->resource->settings()->whereIn('key', $keys)->pluck('value', 'key')->all();
+
+ // Status info
+ $state = UsersController::objectState($this->resource);
+ $statusInfo = UsersController::statusInfo($this->resource);
+
+ // Information about wallets and accounts for access checks
+ $wallets = $this->resource->wallets->map([$this, 'walletPropsMap'])->toArray();
+ $accounts = $this->resource->accounts->map([$this, 'walletPropsMap'])->toArray();
+ $wallet = $this->walletPropsMap($wallet);
+
+ return [
+ // User identifier
+ 'id' => $this->resource->id,
+ // User email address
+ 'email' => $this->resource->email,
+ // User status
+ 'status' => $this->resource->status,
+ // User creation date-time
+ 'created_at' => $this->resource->created_at,
+ // User deletion date-time
+ 'deleted_at' => $this->resource->deleted_at,
+
+ // @var bool Is user active?
+ 'isActive' => $state['isActive'] ?? false,
+ // @var bool Is user deleted?
+ 'isDeleted' => $state['isDeleted'] ?? false,
+ // @var bool Is user degraded?
+ 'isDegraded' => $state['isDegraded'] ?? false,
+ // @var bool Is account owner degraded?
+ 'isAccountDegraded' => $state['isAccountDegraded'] ?? false,
+ // @var bool Readiness state
+ 'isReady' => $state['isReady'],
+ // @var bool IMAP readiness state
+ 'isImapReady' => $state['isImapReady'] ?? false,
+ // @var bool LDAP readiness state
+ 'isLdapReady' => $this->when(isset($state['isLdapReady']), $state['isLdapReady'] ?? false),
+ // @var bool Is user locked?
+ 'isLocked' => $isLocked,
+ // @var bool Is user restricted?
+ 'isRestricted' => $state['isRestricted'] ?? false,
+ // @var bool Is user suspended?
+ 'isSuspended' => $state['isSuspended'] ?? false,
+
+ // @var array<strig, mixed> User settings (first_name, last_name, phone, etc.)
+ 'settings' => $settings,
+ // @var array Wallets controlled by the user
+ 'accounts' => $accounts,
+ // @var array Wallets owned by the user
+ 'wallets' => $wallets,
+ // @var array Wallet the user is in
+ 'wallet' => $wallet,
+ // @var array Extended status/permissions information
+ 'statusInfo' => $statusInfo,
+ ];
+ }
+
+ /**
+ * Add more info to the wallet object output
+ */
+ public function walletPropsMap($wallet): array
+ {
+ $result = $wallet->toArray();
+
+ if ($wallet->discount) {
+ $result['discount'] = $wallet->discount->discount;
+ $result['discount_description'] = $wallet->discount->description;
+ }
+
+ if ($wallet->user_id != $this->resource->id) {
+ // FIXME: This one probably is relevant for an admin/reseller UI only
+ $result['user_email'] = $wallet->owner->email;
+ }
+
+ $provider = PaymentProvider::factory($wallet);
+ $result['provider'] = $provider->name();
+
+ return $result;
+ }
+}
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -61,8 +61,13 @@
use App\UserSetting;
use App\VerificationCode;
use App\Wallet;
+use Dedoc\Scramble\Scramble;
+use Dedoc\Scramble\Support\Generator\OpenApi;
+use Dedoc\Scramble\Support\Generator\SecurityScheme;
use GuzzleHttp\TransferStats;
use Illuminate\Database\Query\Builder;
+use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
@@ -153,6 +158,33 @@
Schema::defaultStringLength(191);
+ Blade::precompiler(static function ($str) {
+ // For Scramble (API documentation) we replace external js/css resources with locally stored
+ if (str_starts_with(\request()->path(), 'docs/api')) {
+ if (preg_match_all('~(src|href)="(https://unpkg.com/[^"]+)~', $str, $matches)) {
+ if (!file_exists(\public_path('vendor/scramble'))) {
+ mkdir(\public_path('vendor/scramble'));
+ }
+
+ foreach ($matches[2] as $href) {
+ $file = 'vendor/scramble/' . md5($href) . '.' . pathinfo($href, \PATHINFO_BASENAME);
+ if (!file_exists(\public_path($file))) {
+ $content = \file_get_contents($href);
+ if (!$content) {
+ break;
+ }
+
+ \file_put_contents(\public_path($file), $content);
+ }
+
+ $str = str_replace($href, "/{$file}", $str);
+ }
+ }
+ }
+
+ return $str;
+ });
+
// Register some template helpers
Blade::directive(
'theme_asset',
@@ -233,6 +265,22 @@
return Http::getFacadeRoot();
});
+ JsonResource::withoutWrapping();
+
+ Scramble::configure()
+ ->routes(static function (Route $route) {
+ // Exclude admin/reseller/webhooks groups for now
+ // TODO: Find a way to separate or describe access
+ return empty($route->action['domain'])
+ && str_starts_with($route->uri, 'api/')
+ && $route->action['prefix'] != 'api/webhooks';
+ })
+ ->withDocumentTransformers(static function (OpenApi $openApi) {
+ $openApi->secure(
+ SecurityScheme::http('bearer')
+ );
+ });
+
$this->applyOverrideConfig();
}
}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -29,13 +29,13 @@
/**
* The eloquent definition of a User.
*
- * @property string $email
- * @property int $id
- * @property string $password
- * @property string $password_ldap
- * @property ?string $role
- * @property int $status
- * @property int $tenant_id
+ * @property string $email User email address
+ * @property int $id User identifier
+ * @property string $password User password
+ * @property string $password_ldap User LDAP password
+ * @property ?string $role User role
+ * @property int $status User status
+ * @property int $tenant_id Tenant identifier
*/
class User extends Authenticatable
{
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -49,10 +49,8 @@
*
* @param string $ip IP address
* @param string $fallback Fallback country code
- *
- * @return string
*/
- public static function countryForIP($ip, $fallback = 'CH')
+ public static function countryForIP($ip, $fallback = 'CH'): string
{
if (!str_contains($ip, ':')) {
// Skip the query if private network
diff --git a/src/composer.json b/src/composer.json
--- a/src/composer.json
+++ b/src/composer.json
@@ -37,7 +37,8 @@
"sabre/vobject": "^4.5",
"spatie/laravel-translatable": "^6.5",
"spomky-labs/otphp": "~10.0.0",
- "stripe/stripe-php": "^10.7"
+ "stripe/stripe-php": "^10.7",
+ "dedoc/scramble": "~0.10"
},
"require-dev": {
"code-lts/doctum": "dev-main",
diff --git a/src/config/scramble.php b/src/config/scramble.php
new file mode 100644
--- /dev/null
+++ b/src/config/scramble.php
@@ -0,0 +1,91 @@
+<?php
+
+use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
+
+return [
+ /*
+ * Your API path. By default, all routes starting with this path will be added to the docs.
+ * If you need to change this behavior, you can add your custom routes resolver using `Scramble::routes()`.
+ */
+ 'api_path' => 'api',
+
+ /*
+ * Your API domain. By default, app domain is used. This is also a part of the default API routes
+ * matcher, so when implementing your own, make sure you use this config if needed.
+ */
+ 'api_domain' => null,
+
+ // The path where your OpenAPI specification will be exported.
+ 'export_path' => 'api.json',
+
+ 'info' => [
+ // API version.
+ 'version' => env('API_VERSION', '4.0'),
+
+ // Description rendered on the home page of the API documentation (`/docs/api`).
+ 'description' => '',
+ ],
+
+ // Customize Stoplight Elements UI
+ 'ui' => [
+ // Define the title of the documentation's website. App name is used when this config is `null`.
+ 'title' => null,
+
+ // Define the theme of the documentation. Available options are `light`, `dark`, and `system`.
+ 'theme' => 'light',
+
+ // Hide the `Try It` feature. Enabled by default.
+ 'hide_try_it' => false,
+
+ // Hide the schemas in the Table of Contents. Enabled by default.
+ 'hide_schemas' => false,
+
+ // URL to an image that displays as a small square logo next to the title, above the table of contents.
+ 'logo' => '',
+
+ // Use to fetch the credential policy for the Try It feature. Options are: omit, include (default), and same-origin
+ 'try_it_credentials_policy' => 'include',
+
+ /*
+ * There are three layouts for Elements:
+ * - sidebar - (Elements default) Three-column design with a sidebar that can be resized.
+ * - responsive - Like sidebar, except at small screen sizes it collapses the sidebar into a drawer that can be toggled open.
+ * - stacked - Everything in a single column, making integrations with existing websites that have their own sidebar or other columns already.
+ */
+ 'layout' => 'responsive',
+ ],
+
+ /*
+ * The list of servers of the API. By default, when `null`, server URL will be created from
+ * `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you
+ * will need to specify the local server URL manually (if needed).
+ *
+ * Example of non-default config (final URLs are generated using Laravel `url` helper):
+ *
+ * ```php
+ * 'servers' => [
+ * 'Live' => 'api',
+ * 'Prod' => 'https://scramble.dedoc.co/api',
+ * ],
+ * ```
+ */
+ 'servers' => null,
+
+ /*
+ * Determines how Scramble stores the descriptions of enum cases.
+ * Available options:
+ * - 'description' – Case descriptions are stored as the enum schema's description using table formatting.
+ * - 'extension' – Case descriptions are stored in the `x-enumDescriptions` enum schema extension.
+ * - false - Case descriptions are ignored.
+ *
+ * @see https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-enum-descriptions
+ */
+ 'enum_cases_description_strategy' => 'description',
+
+ 'middleware' => [
+ 'web',
+ // RestrictedDocsAccess::class,
+ ],
+
+ 'extensions' => [],
+];
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
@@ -124,8 +124,8 @@
localStorage.setItem('token', response.access_token)
localStorage.setItem('refreshToken', response.refresh_token)
- if (response.email) {
- this.authInfo = response
+ if (response.user) {
+ this.authInfo = response.user
}
routerState.isLocked = this.isUser && this.authInfo && this.authInfo.isLocked
diff --git a/src/resources/vue/App.vue b/src/resources/vue/App.vue
--- a/src/resources/vue/App.vue
+++ b/src/resources/vue/App.vue
@@ -29,9 +29,9 @@
const token = localStorage.getItem('token')
if (token) {
- const post = { refresh_token: localStorage.getItem("refreshToken") }
+ const post = { refresh_token: localStorage.getItem("refreshToken"), info: 1 }
- axios.post('/api/auth/info?refresh=1', post, { ignoreErrors: true, loader: true })
+ axios.post('/api/auth/refresh', post, { ignoreErrors: true, loader: true })
.then(response => {
this.$root.loginUser(response.data, false)
})
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -37,7 +37,6 @@
['middleware' => ['auth:api', 'scope:api']],
static function () {
Route::get('info', [API\AuthController::class, 'info']);
- Route::post('info', [API\AuthController::class, 'info']);
Route::get('location', [API\AuthController::class, 'location']);
Route::post('logout', [API\AuthController::class, 'logout']);
Route::post('refresh', [API\AuthController::class, 'refresh']);
diff --git a/src/tests/Feature/Controller/AuthTest.php b/src/tests/Feature/Controller/AuthTest.php
--- a/src/tests/Feature/Controller/AuthTest.php
+++ b/src/tests/Feature/Controller/AuthTest.php
@@ -82,24 +82,6 @@
$this->assertTrue(!isset($json['access_token']));
// Note: Details of the content are tested in testUserResponse()
-
- // Test token refresh via the info request
- // First we log in to get the refresh token
- $post = ['email' => 'john@kolab.org', 'password' => 'simple123'];
- $user = $this->getTestUser('john@kolab.org');
- $response = $this->post("api/auth/login", $post);
- $json = $response->json();
- $response = $this->actingAs($user)
- ->post("api/auth/info?refresh=1", ['refresh_token' => $json['refresh_token']]);
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertSame('john@kolab.org', $json['email']);
- $this->assertTrue(is_array($json['statusInfo']));
- $this->assertTrue(is_array($json['settings']));
- $this->assertTrue(!empty($json['access_token']));
- $this->assertTrue(!empty($json['expires_in']));
}
/**
@@ -181,9 +163,9 @@
);
$this->assertSame('bearer', $json['token_type']);
$this->assertSame($user->id, $json['id']);
- $this->assertSame($user->email, $json['email']);
- $this->assertTrue(is_array($json['statusInfo']));
- $this->assertTrue(is_array($json['settings']));
+ $this->assertSame($user->email, $json['user']['email']);
+ $this->assertTrue(is_array($json['user']['statusInfo']));
+ $this->assertTrue(is_array($json['user']['settings']));
// Valid long password (255 chars)
$password = str_repeat('123abc789E', 25) . '12345';
@@ -212,9 +194,7 @@
$this->assertTrue(!empty($json['id']));
$this->assertTrue(!empty($json['access_token']));
- $this->assertTrue(empty($json['settings']));
- $this->assertTrue(empty($json['statusInfo']));
- $this->assertTrue(empty($json['wallets']));
+ $this->assertTrue(empty($json['user']));
// TODO: We have browser tests for 2FA but we should probably also test it here
@@ -335,13 +315,16 @@
$user = $this->getTestUser('john@kolab.org');
- // Request with a valid token
- $response = $this->actingAs($user)->post("api/auth/refresh", ['refresh_token' => $json['refresh_token']]);
+ // Request with a valid token (include user info in the response)
+ $post = ['refresh_token' => $json['refresh_token'], 'info' => 1];
+ $response = $this->actingAs($user)->post("api/auth/refresh", $post);
$response->assertStatus(200);
$json = $response->json();
- $this->assertTrue(!empty($json['access_token']));
+ $this->assertSame('john@kolab.org', $json['user']['email']);
+ $this->assertTrue(is_array($json['user']['statusInfo']));
+ $this->assertTrue(is_array($json['user']['settings']));
$this->assertTrue($json['access_token'] != $token);
$this->assertTrue(
($this->expectedExpiry - 5) < $json['expires_in']
diff --git a/src/tests/Feature/Controller/DeviceTest.php b/src/tests/Feature/Controller/DeviceTest.php
--- a/src/tests/Feature/Controller/DeviceTest.php
+++ b/src/tests/Feature/Controller/DeviceTest.php
@@ -58,6 +58,7 @@
$json = $response->json();
$this->assertStringContainsString(date('Y-m-d'), $json['created_at']);
+ $this->assertSame([], $json['plans']);
$device = Device::where('hash', $this->hash)->first();
$this->assertTrue(!empty($device));
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -134,11 +134,13 @@
$this->assertSame($individual->title, $json['plans'][0]['title']);
$this->assertSame($individual->name, $json['plans'][0]['name']);
$this->assertSame($individual->description, $json['plans'][0]['description']);
+ $this->assertSame(990, $json['plans'][0]['cost']);
$this->assertFalse($json['plans'][0]['isDomain']);
$this->assertArrayHasKey('button', $json['plans'][0]);
$this->assertSame($group->title, $json['plans'][1]['title']);
$this->assertSame($group->name, $json['plans'][1]['name']);
$this->assertSame($group->description, $json['plans'][1]['description']);
+ $this->assertSame(990, $json['plans'][1]['cost']);
$this->assertTrue($json['plans'][1]['isDomain']);
$this->assertArrayHasKey('button', $json['plans'][1]);
}
@@ -678,7 +680,7 @@
$this->assertSame('bearer', $json['token_type']);
$this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0);
$this->assertNotEmpty($json['access_token']);
- $this->assertSame($identity, $json['email']);
+ $this->assertSame($identity, $json['user']['email']);
Queue::assertPushed(CreateJob::class, 1);
@@ -813,7 +815,7 @@
$this->assertSame('bearer', $result['token_type']);
$this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0);
$this->assertNotEmpty($result['access_token']);
- $this->assertSame("{$login}@{$domain}", $result['email']);
+ $this->assertSame("{$login}@{$domain}", $result['user']['email']);
Queue::assertPushed(\App\Jobs\Domain\CreateJob::class, 1);
@@ -916,8 +918,8 @@
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertNotEmpty($json['access_token']);
- $this->assertSame('test-inv@kolabnow.com', $json['email']);
- $this->assertTrue($json['isLocked']);
+ $this->assertSame('test-inv@kolabnow.com', $json['user']['email']);
+ $this->assertTrue($json['user']['isLocked']);
$user = User::where('email', 'test-inv@kolabnow.com')->first();
$this->assertNotEmpty($user);
$this->assertSame($plan->id, $user->getSetting('plan_id'));
@@ -965,7 +967,7 @@
$this->assertSame('bearer', $result['token_type']);
$this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0);
$this->assertNotEmpty($result['access_token']);
- $this->assertSame('test-inv@kolabnow.com', $result['email']);
+ $this->assertSame('test-inv@kolabnow.com', $result['user']['email']);
// Check if the user has been created
$user = User::where('email', 'test-inv@kolabnow.com')->first();
@@ -1028,7 +1030,7 @@
$json = $response->json();
$this->assertSame('success', $json['status']);
- $this->assertSame('test-inv@kolabnow.com', $json['email']);
+ $this->assertSame('test-inv@kolabnow.com', $json['user']['email']);
// Check if the user has been created
$user = User::where('email', 'test-inv@kolabnow.com')->first();
@@ -1100,7 +1102,7 @@
$this->assertNotEmpty($json['access_token']);
// Check the reference to the code and discount
- $user = User::where('email', $json['email'])->first();
+ $user = User::where('email', $json['user']['email'])->first();
$this->assertSame(1, $referral_code->referrals()->where('user_id', $user->id)->count());
$this->assertSame($discount->id, $user->wallets()->first()->discount_id);
}
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
@@ -5,6 +5,7 @@
use App\Discount;
use App\Domain;
use App\Http\Controllers\API\V4\UsersController;
+use App\Http\Resources\UserInfoResource;
use App\Jobs\User\CreateJob;
use App\Package;
use App\Plan;
@@ -1418,14 +1419,15 @@
/**
* Test user data response used in show and info actions
*/
- public function testUserResponse(): void
+ public function testUserInfoResponse(): void
{
$provider = \config('services.payment_provider') ?: 'mollie';
$john = $this->getTestUser('john@kolab.org');
$wallet = $john->wallets()->first();
$wallet->setSettings(['mollie_id' => null, 'stripe_id' => null]);
$wallet->owner->setSettings(['plan_id' => null]);
- $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]);
+
+ $result = (new UserInfoResource($john))->toArray(\request());
$this->assertSame($john->id, $result['id']);
$this->assertSame($john->email, $result['email']);
@@ -1460,7 +1462,8 @@
$wallet->owner->setSettings(['plan_id' => $plan->id]);
$ned = $this->getTestUser('ned@kolab.org');
$ned_wallet = $ned->wallets()->first();
- $result = $this->invokeMethod(new UsersController(), 'userResponse', [$ned]);
+
+ $result = (new UserInfoResource($ned))->toArray(\request());
$this->assertSame($ned->id, $result['id']);
$this->assertSame($ned->email, $result['email']);
@@ -1492,7 +1495,7 @@
$wallet->setSetting($mod_provider . '_id', 123);
$john->refresh();
- $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]);
+ $result = (new UserInfoResource($john))->toArray(\request());
$this->assertSame($john->id, $result['id']);
$this->assertSame($discount->id, $result['wallet']['discount_id']);
@@ -1509,7 +1512,8 @@
$jack = $this->getTestUser('jack@kolab.org');
$jack->status |= User::STATUS_ACTIVE;
$jack->save();
- $result = $this->invokeMethod(new UsersController(), 'userResponse', [$jack]);
+
+ $result = (new UserInfoResource($jack))->toArray(\request());
$this->assertFalse($result['statusInfo']['enableDomains']);
$this->assertFalse($result['statusInfo']['enableWallets']);
@@ -1524,7 +1528,8 @@
// Test locked user
$john->status &= ~User::STATUS_ACTIVE;
$john->save();
- $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]);
+
+ $result = (new UserInfoResource($john))->toArray(\request());
$this->assertTrue($result['isLocked']);
}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 2:42 PM (11 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18824335
Default Alt Text
D5574.1775227340.diff (53 KB)

Event Timeline