Page MenuHomePhorge

D5601.1775334668.diff
No OneTemporary

Authored By
Unknown
Size
22 KB
Referenced Files
None
Subscribers
None

D5601.1775334668.diff

diff --git a/src/app/Device.php b/src/app/Device.php
--- a/src/app/Device.php
+++ b/src/app/Device.php
@@ -2,7 +2,6 @@
namespace App;
-use App\Http\Resources\PlanResource;
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
use App\Traits\UuidIntKeyTrait;
@@ -52,46 +51,42 @@
}
/**
- * Get device information
+ * Signup a device
*/
- 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 plan/wallet/payments state
-
- return $result;
- }
-
- /**
- * Register a device
- */
- public static function register(string $hash): self
+ public static function signup(string $token, Plan $plan, string $password): self
{
DB::beginTransaction();
// Create a device record
- $device = self::create(['hash' => $hash]);
-
- // Create a special account (and wallet)
- $user = User::create([
- 'email' => $device->id . '@' . \config('app.domain'),
- 'password' => '',
- 'role' => User::ROLE_DEVICE,
+ $device = self::create(['hash' => $token]);
+
+ // Create a special account
+ while (true) {
+ $user_id = Utils::uuidInt();
+ if (!User::withTrashed()->where('id', $user_id)->orWhere('email', $user_id . '@' . \config('app.domain'))->exists()) {
+ break;
+ }
+ }
+
+ $user = new User();
+ $user->id = $user_id;
+ $user->email = $user_id . '@' . \config('app.domain');
+ $user->password = $password;
+ $user->role = User::ROLE_DEVICE;
+ $user->save();
+
+ $user->settings()->insert([
+ ['key' => 'signup_token', 'value' => $token, 'user_id' => $user->id],
+ ['key' => 'plan_id', 'value' => $plan->id, 'user_id' => $user->id],
]);
- // Assign an entitlement
+ // Assign the device via an entitlement
+ // TODO: Use the device SKU from the plan package?
+ // TODO: Push entitlements.updated_at to one year in the future
$device->assignToWallet($user->wallets()->first(), 'device');
+ // TODO: Trigger payment mandate creation?
+
DB::commit();
return $device;
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
@@ -26,11 +26,9 @@
*
* Note that the same information is by default included in the `auth/login` response.
*/
- public function info(): JsonResponse
+ public function info()
{
- $response = new UserInfoResource($this->guard()->user());
-
- return $response->response();
+ return new UserInfoResource($this->guard()->user());
}
/**
@@ -41,7 +39,7 @@
* @param string $password Plain text password
* @param string|null $secondFactor Second factor code if available
*/
- public static function logonResponse(User $user, string $password, ?string $secondFactor = null): JsonResponse
+ public static function logonResponse(User $user, string $password, ?string $secondFactor = null)
{
$mode = request()->mode; // have to be before we make a request below
@@ -69,7 +67,7 @@
*
* @unauthenticated
*/
- public function login(Request $request): JsonResponse
+ public function login(Request $request)
{
$v = Validator::make(
$request->all(),
@@ -175,7 +173,7 @@
/**
* Refresh a session token.
*/
- public function refresh(Request $request): JsonResponse
+ public function refresh(Request $request)
{
$v = Validator::make($request->all(), [
// Request user information in the response
@@ -209,7 +207,7 @@
* @param ?User $user The user being authenticated
* @param ?bool $mode Response mode: 'fast' - return minimum set of user data
*/
- protected static function respondWithToken($tokenResponse, $user = null, $mode = null): JsonResponse
+ protected static function respondWithToken($tokenResponse, $user = null, $mode = null)
{
$data = json_decode($tokenResponse->getContent());
@@ -247,6 +245,6 @@
}
}
- return $response->response();
+ return $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
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
+use App\Http\Resources\AuthResource;
use App\Jobs\Mail\PasswordResetJob;
use App\Rules\Password;
use App\User;
@@ -124,7 +125,7 @@
* @unauthenticated
*/
#[BodyParameter('secondfactor', description: '2FA token (required if user enabled 2FA)', type: 'string')]
- public function reset(Request $request): JsonResponse
+ public function reset(Request $request)
{
$v = $this->verify($request);
if ($v->status() !== 200) {
@@ -158,7 +159,7 @@
*/
#[BodyParameter('email', description: 'User email address', type: 'string', required: true)]
#[BodyParameter('secondfactor', description: '2FA token (required if user enabled 2FA)', type: 'string')]
- public function resetExpired(Request $request): JsonResponse
+ public function resetExpired(Request $request)
{
$user = User::where('email', $request->email)->first();
@@ -269,7 +270,7 @@
// could be possibly a bit simpler (no need for a DB transaction).
$response = AuthController::logonResponse($user, $request->password, $request->secondfactor);
- if ($response->status() == 200) {
+ if ($response instanceof AuthResource) {
// Remove the verification code
if ($request->code instanceof VerificationCode) {
$request->code->delete();
@@ -278,9 +279,7 @@
DB::commit();
// Add confirmation message to the 'success' response
- $data = $response->getData(true);
- $data['message'] = self::trans('app.password-reset-success');
- $response->setData($data);
+ $response->message = self::trans('app.password-reset-success');
} else {
// If authentication failed (2FA or geo-lock), revert the password change
DB::rollBack();
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
@@ -382,7 +382,7 @@
#[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 signup(Request $request): JsonResponse
+ public function signup(Request $request)
{
$v = $this->signupValidate($request);
if ($v->status() !== 200) {
@@ -477,10 +477,7 @@
$response = AuthController::logonResponse($user, $request->password);
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);
+ $response->checkout = $this->mandateForPlan($request->plan, $request->discount, $user);
}
return $response;
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
@@ -3,24 +3,31 @@
namespace App\Http\Controllers\API\V4;
use App\Device;
+use App\Http\Controllers\API\AuthController;
use App\Http\Controllers\Controller;
-use App\SignupToken;
+use App\Http\Resources\DeviceInfoResource;
+use App\Plan;
+use App\Rules\SignupToken as SignupTokenRule;
+use App\Utils;
+use Dedoc\Scramble\Attributes\BodyParameter;
use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
class DeviceController extends Controller
{
/**
- * User claims the device ownership.
+ * Claim a device.
*
- * @param string $hash Device secret identifier
+ * @param string $token Device secret token
*/
- public function claim(string $hash): JsonResponse
+ public function claim(string $token): JsonResponse
{
- if (strlen($hash) > 191) {
+ if (strlen($token) > 191) {
return $this->errorResponse(404);
}
- $device = Device::where('hash', strtoupper($hash))->first();
+ $device = Device::where('hash', strtoupper($token))->first();
if (empty($device)) {
return $this->errorResponse(404);
@@ -35,32 +42,88 @@
}
/**
- * Get the device information.
+ * Device information.
*
- * @param string $hash Device secret identifier
+ * @param string $token Device secret token
*
* @unauthenticated
*/
- public function info(string $hash): JsonResponse
+ public function info(string $token): JsonResponse
{
- if (strlen($hash) > 191) {
+ if (strlen($token) > 191) {
return $this->errorResponse(404);
}
- $device = Device::where('hash', $hash)->first();
+ // @var Device $device
+ $device = Device::where('hash', $token)->first();
- // Register a device
if (!$device) {
- // Only possible if a signup token exists?
- if (!SignupToken::where('id', strtoupper($hash))->exists()) {
- return $this->errorResponse(404);
- }
+ return $this->errorResponse(404);
+ }
+
+ return (new DeviceInfoResource($device))->response();
+ }
+
+ /**
+ * Signup a device.
+ *
+ * @param string $token Device secret token
+ *
+ * @unauthenticated
+ */
+ #[BodyParameter('plan', description: 'Plan title', type: 'string', required: true)]
+ public function signup(Request $request, string $token)
+ {
+ $v = Validator::make($request->all(), ['plan' => ['required', 'string']]);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ // Signup plan
+ $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
+
+ if (!$plan) {
+ $errors = ['plan' => self::trans('validation.invalidvalue')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ $request->merge([
+ 'plan' => $plan,
+ 'token' => \strtoupper($token),
+ ]);
+
+ // Validate input
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'token' => ['required', 'string', new SignupTokenRule($plan)],
+ ]
+ );
- $device = Device::register($hash);
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', /* @var array */ 'errors' => $v->errors()], 422);
}
- $response = $device->info();
+ // Check if a device already exists
+ if (Device::withTrashed()->where('hash', $token)->exists()) {
+ return $this->errorResponse(500);
+ }
+
+ $password = Utils::generatePassphrase();
+
+ // Register a device
+ $device = Device::signup($token, $plan, $password);
+
+ // Auto-login the user (same as we do on a normal user signup)
+ $response = AuthController::logonResponse($device->account, $password);
+
+ // Let the device know email+password so it can use the API
+ $response->credentials = [
+ 'email' => $device->account->email,
+ 'password' => $password,
+ ];
- return response()->json($response);
+ return $response;
}
}
diff --git a/src/app/Http/Resources/AuthResource.php b/src/app/Http/Resources/AuthResource.php
--- a/src/app/Http/Resources/AuthResource.php
+++ b/src/app/Http/Resources/AuthResource.php
@@ -11,7 +11,10 @@
class AuthResource extends JsonResource
{
public string $status = 'success';
+ public ?string $message = null;
public ?int $user_id = null;
+ public ?array $checkout = null;
+ public ?array $credentials = null;
private ?UserInfoResource $userinfo = null;
@@ -42,6 +45,12 @@
'expires_in' => (int) $this->resource->expires_in,
// Response status
'status' => $this->status,
+ // @var string Response message
+ 'message' => $this->when(isset($this->message), $this->message),
+ // @var array Payment checkout information (on signup)
+ 'checkout' => $this->when(isset($this->checkout), $this->checkout),
+ // @var array New user credentials (on device signup)
+ 'credentials' => $this->when(isset($this->credentials), $this->credentials),
// @var int User identifier
'id' => $this->user_id,
// @var UserInfoResource User information
diff --git a/src/app/Http/Resources/DeviceInfoResource.php b/src/app/Http/Resources/DeviceInfoResource.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Resources/DeviceInfoResource.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Http\Resources;
+
+use App\Device;
+use App\Plan;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+/**
+ * Device response
+ *
+ * @mixin Device
+ */
+class DeviceInfoResource extends JsonResource
+{
+ /**
+ * Transform the resource into an array.
+ */
+ public function toArray(Request $request): array
+ {
+ // TODO: Get plan identifiers from SignupTokens?
+
+ $plans = Plan::withObjectTenantContext($this->resource)
+ ->where('mode', Plan::MODE_TOKEN)
+ ->orderByDesc('months')->orderByDesc('title')
+ ->get();
+
+ return [
+ // Device registration date-time
+ 'created_at' => (string) $this->resource->created_at,
+ // Plans available for signup via a device token
+ 'plans' => PlanResource::collection($plans),
+ ];
+ }
+}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -738,7 +738,7 @@
*/
public function setRoleAttribute($role)
{
- if ($role !== null && !in_array($role, [self::ROLE_ADMIN, self::ROLE_RESELLER, self::ROLE_SERVICE])) {
+ if ($role !== null && !in_array($role, [self::ROLE_ADMIN, self::ROLE_RESELLER, self::ROLE_SERVICE, self::ROLE_DEVICE])) {
throw new \Exception("Invalid role: {$role}");
}
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -197,8 +197,10 @@
Route::get('config/webmail', [API\V4\ConfigController::class, 'webmail']);
- Route::post('device/{hash}/claim', [API\V4\DeviceController::class, 'claim']);
- Route::get('device/{hash}', [API\V4\DeviceController::class, 'info'])
+ Route::post('device/{token}/claim', [API\V4\DeviceController::class, 'claim']);
+ Route::get('device/{token}', [API\V4\DeviceController::class, 'info'])
+ ->withoutMiddleware(['auth:api', 'scope:api']);
+ Route::post('device/{token}/signup', [API\V4\DeviceController::class, 'signup'])
->withoutMiddleware(['auth:api', 'scope:api']);
Route::apiResource('domains', API\V4\DomainsController::class);
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
@@ -18,61 +18,49 @@
parent::setUp();
$this->hash = str_repeat('1', 64);
- SignupToken::query()->delete();
- Device::query()->forceDelete();
User::where('role', User::ROLE_DEVICE)->forceDelete();
$this->deleteTestUser('jane@kolabnow.com');
+ SignupToken::query()->delete();
+ Device::query()->forceDelete();
+ Plan::where('title', 'test')->delete();
}
protected function tearDown(): void
{
- SignupToken::query()->delete();
- Device::query()->forceDelete();
User::where('role', User::ROLE_DEVICE)->forceDelete();
$this->deleteTestUser('jane@kolabnow.com');
+ SignupToken::query()->delete();
+ Device::query()->forceDelete();
+ Plan::where('title', 'test')->delete();
parent::tearDown();
}
/**
- * Test device registration and info (GET /api/v4/device/<hash>)
+ * Test device info (GET /api/v4/device/<token>)
*/
public function testInfo(): void
{
- // Unknown hash (invalid hash)
+ // Unknown hash (invalid token)
$response = $this->get('api/v4/device/unknown');
$response->assertStatus(404);
- // Unknown hash (valid hash)
+ // Unknown hash (valid token)
$response = $this->get('api/v4/device/' . $this->hash);
$response->assertStatus(404);
- $sku = Sku::withEnvTenantContext()->where('title', 'device')->first();
+ // Getting info of a registered device
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
- $plan->signupTokens()->create(['id' => $this->hash]);
+ Device::signup($this->hash, $plan, 'simple123');
- // First registration
$response = $this->get('api/v4/device/' . $this->hash);
$response->assertStatus(200);
$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));
- $entitlements = $device->wallet()->entitlements()->get();
- $this->assertCount(1, $entitlements);
- $this->assertSame($sku->id, $entitlements[0]->sku_id);
-
- // Getting info of a registered device
- $response = $this->get('api/v4/device/' . $this->hash);
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertStringContainsString(date('Y-m-d'), $json['created_at']);
+ // TODO Assert 'plans' response
}
/**
@@ -90,7 +78,8 @@
$response = $this->actingAs($user)->post('api/v4/device/' . $this->hash . '/claim', []);
$response->assertStatus(404);
- $device = Device::register($this->hash);
+ $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
+ $device = Device::signup($this->hash, $plan, 'simple123');
// Claim an existing device
$response = $this->actingAs($user)->post('api/v4/device/' . $this->hash . '/claim', []);
@@ -103,4 +92,73 @@
$this->assertCount(1, $device->entitlements);
$this->assertSame($user->wallets()->first()->id, $device->entitlements[0]->wallet_id);
}
+
+ /**
+ * Test device registration (POST /api/v4/device/<token>/signup)
+ */
+ public function testSignup(): void
+ {
+ $sku = Sku::withEnvTenantContext()->where('title', 'device')->first();
+ $plan = Plan::create([
+ 'title' => 'test',
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'mode' => Plan::MODE_TOKEN,
+ ]);
+
+ // Signup, missing plan
+ $post = [];
+ $response = $this->post("api/v4/device/{$this->hash}/signup", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame(['plan' => ['The plan field is required.']], $json['errors']);
+
+ // Signup, invalid plan
+ $post = ['plan' => 'invalid'];
+ $response = $this->post("api/v4/device/{$this->hash}/signup", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame(['plan' => 'Invalid value'], $json['errors']);
+
+ // Signup, valid plan, but unknown token
+ $post = ['plan' => $plan->title];
+ $response = $this->post("api/v4/device/{$this->hash}/signup", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame(['token' => ['The signup token is invalid.']], $json['errors']);
+
+ $plan->signupTokens()->create(['id' => $this->hash]);
+
+ // Signup success
+ $response = $this->post("api/v4/device/{$this->hash}/signup", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $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']);
+
+ $device = Device::where('hash', $this->hash)->first();
+
+ $this->assertTrue(!empty($device));
+ $this->assertSame($device->account->email, $json['user']['email']);
+ $this->assertSame(User::ROLE_DEVICE, $device->account->role);
+ $this->assertSame($plan->id, $device->account->getSetting('plan_id'));
+ $this->assertSame($this->hash, $device->account->getSetting('signup_token'));
+
+ $entitlements = $device->wallet()->entitlements()->get();
+ $this->assertCount(1, $entitlements);
+ $this->assertSame($sku->id, $entitlements[0]->sku_id);
+ }
}

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 8:31 PM (20 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831083
Default Alt Text
D5601.1775334668.diff (22 KB)

Event Timeline