Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117875818
D5601.1775334668.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
22 KB
Referenced Files
None
Subscribers
None
D5601.1775334668.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D5601: Device signup
Attached
Detach File
Event Timeline