Changeset View
Changeset View
Standalone View
Standalone View
src/app/Http/Controllers/API/V4/CompanionAppsController.php
<?php | <?php | ||||
namespace App\Http\Controllers\API\V4; | namespace App\Http\Controllers\API\V4; | ||||
use App\Http\Controllers\ResourceController; | use App\Http\Controllers\ResourceController; | ||||
use App\Utils; | use App\Utils; | ||||
use App\Tenant; | use App\Tenant; | ||||
use Laravel\Passport\Token; | use Laravel\Passport\Passport; | ||||
use Laravel\Passport\TokenRepository; | use Laravel\Passport\ClientRepository; | ||||
use Laravel\Passport\RefreshTokenRepository; | |||||
use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||
use Illuminate\Support\Facades\Validator; | use Illuminate\Support\Facades\Validator; | ||||
use Illuminate\Support\Str; | |||||
use BaconQrCode; | use BaconQrCode; | ||||
class CompanionAppsController extends ResourceController | class CompanionAppsController extends ResourceController | ||||
{ | { | ||||
/** | /** | ||||
* Remove the specified companion app. | |||||
* | |||||
* @param string $id Companion app identifier | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse | |||||
*/ | |||||
public function destroy($id) | |||||
{ | |||||
$companion = \App\CompanionApp::find($id); | |||||
if (!$companion) { | |||||
return $this->errorResponse(404); | |||||
} | |||||
$user = $this->guard()->user(); | |||||
if ($user->id != $companion->user_id) { | |||||
return $this->errorResponse(403); | |||||
} | |||||
// Revoke client and tokens | |||||
$client = $companion->passportClient(); | |||||
if ($client) { | |||||
$clientRepository = app(ClientRepository::class); | |||||
machniak: You call passportClient() two times here. | |||||
$clientRepository->delete($client); | |||||
} | |||||
$companion->delete(); | |||||
return response()->json([ | |||||
'status' => 'success', | |||||
'message' => \trans('app.companion-delete-success'), | |||||
]); | |||||
} | |||||
/** | |||||
* Create a companion app. | |||||
* | |||||
* @param \Illuminate\Http\Request $request | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse | |||||
*/ | |||||
public function store(Request $request) | |||||
{ | |||||
$user = $this->guard()->user(); | |||||
Done Inline ActionsName validation? machniak: Name validation? | |||||
$v = Validator::make( | |||||
$request->all(), | |||||
[ | |||||
'name' => 'required|string|max:512', | |||||
] | |||||
Done Inline ActionsI guess mfa_enabled=false and device_id='' could become defaults on the database level, so we could skip it here. machniak: I guess mfa_enabled=false and device_id='' could become defaults on the database level, so we… | |||||
); | |||||
if ($v->fails()) { | |||||
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | |||||
} | |||||
$app = \App\CompanionApp::create([ | |||||
'name' => $request->name, | |||||
'user_id' => $user->id, | |||||
]); | |||||
return response()->json([ | |||||
'status' => 'success', | |||||
'message' => \trans('app.companion-create-success'), | |||||
'id' => $app->id | |||||
]); | |||||
} | |||||
/** | |||||
* Register a companion app. | * Register a companion app. | ||||
* | * | ||||
* @param \Illuminate\Http\Request $request The API request. | * @param \Illuminate\Http\Request $request The API request. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse The response | * @return \Illuminate\Http\JsonResponse The response | ||||
*/ | */ | ||||
public function register(Request $request) | public function register(Request $request) | ||||
{ | { | ||||
$user = $this->guard()->user(); | $user = $this->guard()->user(); | ||||
$v = Validator::make( | $v = Validator::make( | ||||
$request->all(), | $request->all(), | ||||
[ | [ | ||||
'notificationToken' => 'required|min:4|max:512', | 'notificationToken' => 'required|string|min:4|max:512', | ||||
'deviceId' => 'required|min:4|max:64', | 'deviceId' => 'required|string|min:4|max:64', | ||||
'name' => 'required|max:512', | 'companionId' => 'required|max:64', | ||||
'name' => 'required|string|max:512', | |||||
Done Inline ActionsI'd add |string| to all these rules. machniak: I'd add |string| to all these rules. | |||||
] | ] | ||||
); | ); | ||||
if ($v->fails()) { | if ($v->fails()) { | ||||
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | ||||
} | } | ||||
$notificationToken = $request->notificationToken; | $notificationToken = $request->notificationToken; | ||||
$deviceId = $request->deviceId; | $deviceId = $request->deviceId; | ||||
$companionId = $request->companionId; | |||||
$name = $request->name; | $name = $request->name; | ||||
\Log::info("Registering app. Notification token: {$notificationToken} Device id: {$deviceId} Name: {$name}"); | \Log::info("Registering app. Notification token: {$notificationToken} Device id: {$deviceId} Name: {$name}"); | ||||
$app = \App\CompanionApp::where('device_id', $deviceId)->first(); | $app = \App\CompanionApp::find($companionId); | ||||
if (!$app) { | if (!$app) { | ||||
$app = new \App\CompanionApp(); | return $this->errorResponse(404); | ||||
$app->user_id = $user->id; | } | ||||
$app->device_id = $deviceId; | |||||
$app->mfa_enabled = true; | |||||
$app->name = $name; | |||||
} else { | |||||
//FIXME this allows a user to probe for another users deviceId | |||||
if ($app->user_id != $user->id) { | if ($app->user_id != $user->id) { | ||||
\Log::warning("User mismatch on device registration. Expected {$user->id} but found {$app->user_id}"); | \Log::warning("User mismatch on device registration. Expected {$user->id} but found {$app->user_id}"); | ||||
return $this->errorResponse(403); | return $this->errorResponse(403); | ||||
} | } | ||||
} | |||||
$app->device_id = $deviceId; | |||||
$app->mfa_enabled = true; | |||||
$app->name = $name; | |||||
$app->notification_token = $notificationToken; | $app->notification_token = $notificationToken; | ||||
$app->save(); | $app->save(); | ||||
return response()->json(['status' => 'success']); | return response()->json(['status' => 'success']); | ||||
} | } | ||||
/** | /** | ||||
* Generate a QR-code image for a string | * Generate a QR-code image for a string | ||||
* | * | ||||
* @param string $data data to encode | * @param string $data data to encode | ||||
* | * | ||||
* @return string | * @return string | ||||
*/ | */ | ||||
private static function generateQRCode($data) | private static function generateQRCode($data) | ||||
{ | { | ||||
$renderer_style = new BaconQrCode\Renderer\RendererStyle\RendererStyle(300, 1); | $renderer_style = new BaconQrCode\Renderer\RendererStyle\RendererStyle(300, 1); | ||||
$renderer_image = new BaconQrCode\Renderer\Image\SvgImageBackEnd(); | $renderer_image = new BaconQrCode\Renderer\Image\SvgImageBackEnd(); | ||||
$renderer = new BaconQrCode\Renderer\ImageRenderer($renderer_style, $renderer_image); | $renderer = new BaconQrCode\Renderer\ImageRenderer($renderer_style, $renderer_image); | ||||
$writer = new BaconQrCode\Writer($renderer); | $writer = new BaconQrCode\Writer($renderer); | ||||
return 'data:image/svg+xml;base64,' . base64_encode($writer->writeString($data)); | return 'data:image/svg+xml;base64,' . base64_encode($writer->writeString($data)); | ||||
} | } | ||||
/** | /** | ||||
* Revoke all companion app devices. | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse The response | |||||
*/ | |||||
public function revokeAll() | |||||
{ | |||||
$user = $this->guard()->user(); | |||||
\App\CompanionApp::where('user_id', $user->id)->delete(); | |||||
// Revoke all companion app tokens | |||||
$clientIdentifier = \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_id'); | |||||
$tokens = Token::where('user_id', $user->id)->where('client_id', $clientIdentifier)->get(); | |||||
$tokenRepository = app(TokenRepository::class); | |||||
$refreshTokenRepository = app(RefreshTokenRepository::class); | |||||
foreach ($tokens as $token) { | |||||
$tokenRepository->revokeAccessToken($token->id); | |||||
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($token->id); | |||||
} | |||||
return response()->json([ | |||||
'status' => 'success', | |||||
'message' => \trans("app.companion-deleteall-success"), | |||||
]); | |||||
} | |||||
/** | |||||
* List devices. | * List devices. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse | * @return \Illuminate\Http\JsonResponse | ||||
*/ | */ | ||||
public function index() | public function index() | ||||
{ | { | ||||
$user = $this->guard()->user(); | $user = $this->guard()->user(); | ||||
$search = trim(request()->input('search')); | $search = trim(request()->input('search')); | ||||
Show All 11 Lines | public function index() | ||||
if (count($result) > $pageSize) { | if (count($result) > $pageSize) { | ||||
$result->pop(); | $result->pop(); | ||||
$hasMore = true; | $hasMore = true; | ||||
} | } | ||||
// Process the result | // Process the result | ||||
$result = $result->map( | $result = $result->map( | ||||
function ($device) { | function ($device) { | ||||
return $device->toArray(); | return array_merge($device->toArray(), [ | ||||
'isReady' => $device->isPaired() | |||||
]); | |||||
} | } | ||||
); | ); | ||||
$result = [ | $result = [ | ||||
'list' => $result, | 'list' => $result, | ||||
'count' => count($result), | 'count' => count($result), | ||||
'hasMore' => $hasMore, | 'hasMore' => $hasMore, | ||||
]; | ]; | ||||
Show All 15 Lines | public function show($id) | ||||
return $this->errorResponse(404); | return $this->errorResponse(404); | ||||
} | } | ||||
$user = $this->guard()->user(); | $user = $this->guard()->user(); | ||||
if ($user->id != $result->user_id) { | if ($user->id != $result->user_id) { | ||||
return $this->errorResponse(403); | return $this->errorResponse(403); | ||||
} | } | ||||
return response()->json($result->toArray()); | return response()->json(array_merge($result->toArray(), [ | ||||
'statusInfo' => [ | |||||
'isReady' => $result->isPaired() | |||||
] | |||||
])); | |||||
} | } | ||||
/** | /** | ||||
* Retrieve the pairing information encoded into a qrcode image. | * Retrieve the pairing information encoded into a qrcode image. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse | * @return \Illuminate\Http\JsonResponse | ||||
*/ | */ | ||||
public function pairing() | public function pairing($id) | ||||
{ | { | ||||
$user = $this->guard()->user(); | $result = \App\CompanionApp::find($id); | ||||
if (!$result) { | |||||
return $this->errorResponse(404); | |||||
} | |||||
$clientIdentifier = \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_id'); | $user = $this->guard()->user(); | ||||
$clientSecret = \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_secret'); | if ($user->id != $result->user_id) { | ||||
if (empty($clientIdentifier) || empty($clientSecret)) { | return $this->errorResponse(403); | ||||
\Log::warning("Empty client identifier or secret. Can't generate qr-code."); | |||||
return $this->errorResponse(500); | |||||
} | } | ||||
$client = $result->passportClient(); | |||||
if (!$client) { | |||||
$client = Passport::client()->forceFill([ | |||||
'user_id' => $user->id, | |||||
'name' => "CompanionApp Password Grant Client", | |||||
'secret' => Str::random(40), | |||||
'provider' => 'users', | |||||
'redirect' => 'https://' . \config('app.website_domain'), | |||||
'personal_access_client' => 0, | |||||
'password_client' => 1, | |||||
'revoked' => false, | |||||
'allowed_scopes' => "mfa" | |||||
]); | |||||
$client->save(); | |||||
$result->setPassportClient($client); | |||||
$result->save(); | |||||
} | |||||
Done Inline ActionsAgain, a redundant sql query. machniak: Again, a redundant sql query. | |||||
$response['qrcode'] = self::generateQRCode( | $response['qrcode'] = self::generateQRCode( | ||||
json_encode([ | json_encode([ | ||||
"serverUrl" => Utils::serviceUrl('', $user->tenant_id), | "serverUrl" => Utils::serviceUrl('', $user->tenant_id), | ||||
"clientIdentifier" => \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_id'), | "clientIdentifier" => $client->id, | ||||
"clientSecret" => \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_secret'), | "clientSecret" => $client->secret, | ||||
"companionId" => $id, | |||||
"username" => $user->email | "username" => $user->email | ||||
]) | ]) | ||||
); | ); | ||||
return response()->json($response); | return response()->json($response); | ||||
} | } | ||||
} | } |
You call passportClient() two times here.