New OAUTH2 endpoints to update user info (ME)

PUT /api/v1/users/me

payload

'first_name' => 'sometimes|string',
'last_name' => 'sometimes|string',
'email' => 'sometimes|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'gender_specify' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',

required scopes

me/write

PUT /api/v1/users/me/pic

multiform encoding

pic: file (png, jpg, jpeg)

required scopes

me/write

Change-Id: I31a1edd9eb1fcdee228a8f5ba1b44d324116edd9
Signed-off-by: smarcet <smarcet@gmail.com>
This commit is contained in:
smarcet 2020-08-12 05:42:45 -03:00
parent 55c7d0f9b9
commit 8ff349761d
13 changed files with 438 additions and 86 deletions

View File

@ -1,14 +1,27 @@
<?php
namespace App\Exceptions;
<?php namespace App\Exceptions;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Exception;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Predis\Connection\ConnectionException as RedisConnectionException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
/**
* Class Handler
* @package App\Exceptions
*/
class Handler extends ExceptionHandler
{
/**
@ -21,6 +34,7 @@ class Handler extends ExceptionHandler
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
RedisConnectionException::class,
];
/**

View File

@ -13,11 +13,13 @@
**/
use App\Http\Controllers\GetAllTrait;
use App\Http\Utils\PagingConstants;
use App\Http\Controllers\UserValidationRulesFactory;
use App\Http\Utils\HTMLCleaner;
use App\ModelSerializers\SerializerRegistry;
use Auth\Repositories\IUserRepository;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Validator;
use models\exceptions\EntityNotFoundException;
@ -26,14 +28,10 @@ use OAuth2\Builders\IdTokenBuilder;
use OAuth2\IResourceServerContext;
use OAuth2\Repositories\IClientRepository;
use OAuth2\ResourceServer\IUserService;
use utils\Filter;
use utils\FilterParser;
use Utils\Http\HttpContentType;
use utils\OrderParser;
use utils\PagingInfo;
use Utils\Services\ILogService;
use Exception;
use OpenId\Services\IUserService as IOpenIdUserService;
/**
* Class OAuth2UserApiController
* @package App\Http\Controllers\Api\OAuth2
@ -92,10 +90,18 @@ final class OAuth2UserApiController extends OAuth2ProtectedController
private $id_token_builder;
/**
* @var IOpenIdUserService
*/
private $openid_user_service;
/**
* OAuth2UserApiController constructor.
* @param IUserRepository $repository
* @param IUserService $user_service
* @param IResourceServerContext $resource_server_context
* @param ILogService $log_service
* @param IOpenIdUserService $openid_user_service
* @param IClientRepository $client_repository
* @param IdTokenBuilder $id_token_builder
*/
@ -105,6 +111,7 @@ final class OAuth2UserApiController extends OAuth2ProtectedController
IUserService $user_service,
IResourceServerContext $resource_server_context,
ILogService $log_service,
IOpenIdUserService $openid_user_service,
IClientRepository $client_repository,
IdTokenBuilder $id_token_builder
)
@ -114,6 +121,7 @@ final class OAuth2UserApiController extends OAuth2ProtectedController
$this->user_service = $user_service;
$this->client_repository = $client_repository;
$this->id_token_builder = $id_token_builder;
$this->openid_user_service = $openid_user_service;
}
/**
@ -131,6 +139,88 @@ final class OAuth2UserApiController extends OAuth2ProtectedController
}
}
protected function curateUpdatePayload(array $payload): array
{
// remove possible fields that an user can not update
// from this endpoint
if(isset($payload['groups']))
unset($payload['groups']);
if(isset($payload['email_verified']))
unset($payload['email_verified']);
if(isset($payload['active']))
unset($payload['active']);
return HTMLCleaner::cleanData($payload, [
'bio', 'statement_of_interest'
]);
}
public function UpdateMe(){
try {
if(!Request::isJson()) return $this->error400();
if(!$this->resource_server_context->getCurrentUserId()){
return $this->error403();
}
$payload = Input::json()->all();
// Creates a Validator instance and validates the data.
$validation = Validator::make($payload, UserValidationRulesFactory::build($payload, true));
if ($validation->fails()) {
$ex = new ValidationException();
throw $ex->setMessages($validation->messages()->toArray());
}
$user = $this->openid_user_service->update($this->resource_server_context->getCurrentUserId(), $this->curateUpdatePayload($payload));
return $this->updated(SerializerRegistry::getInstance()->getSerializer($user, SerializerRegistry::SerializerType_Private)->serialize());
}
catch (ValidationException $ex1)
{
Log::warning($ex1);
return $this->error412($ex1->getMessages());
}
catch (EntityNotFoundException $ex2)
{
Log::warning($ex2);
return $this->error404(['message' => $ex2->getMessage()]);
}
catch (Exception $ex) {
Log::error($ex);
return $this->error500($ex);
}
}
public function UpdateMyPic(){
try {
if (!$this->resource_server_context->getCurrentUserId()) {
return $this->error403();
}
$file = request()->file('pic');
if (!is_null($file)) {
$user = $this->openid_user_service->updateProfilePhoto($this->resource_server_context->getCurrentUserId(), $file);
}
return $this->updated(SerializerRegistry::getInstance()->getSerializer($user, SerializerRegistry::SerializerType_Private)->serialize());
}
catch (ValidationException $ex1)
{
Log::warning($ex1);
return $this->error412($ex1->getMessages());
}
catch (EntityNotFoundException $ex2)
{
Log::warning($ex2);
return $this->error404(['message' => $ex2->getMessage()]);
}
catch (Exception $ex) {
Log::error($ex);
return $this->error500($ex);
}
}
public function userInfo()
{
try {

View File

@ -13,6 +13,7 @@
**/
use App\Http\Controllers\APICRUDController;
use App\Http\Controllers\UserValidationRulesFactory;
use App\Http\Utils\HTMLCleaner;
use App\ModelSerializers\SerializerRegistry;
use Auth\Repositories\IUserRepository;
@ -173,36 +174,7 @@ final class UserApiController extends APICRUDController
*/
protected function getUpdatePayloadValidationRules(): array
{
return [
'first_name' => 'required|string',
'last_name' => 'required|string',
'email' => 'required|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'gender_specify' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'email_verified' => 'nullable|boolean',
'active' => 'nullable|boolean',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',
];
return UserValidationRulesFactory::build([], true);
}
protected function curateUpdatePayload(array $payload): array
@ -224,38 +196,9 @@ final class UserApiController extends APICRUDController
*/
protected function getCreatePayloadValidationRules(): array
{
return [
'first_name' => 'required|string',
'last_name' => 'required|string',
'email' => 'required|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'email_verified' => 'nullable|boolean',
'active' => 'nullable|boolean',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',
];
return UserValidationRulesFactory::build([], false);
}
/**
* @param LaravelRequest $request
* @return \Illuminate\Http\JsonResponse|mixed

View File

@ -0,0 +1,96 @@
<?php namespace App\Http\Controllers;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* Class UserValidationRulesFactory
* @package App\Http\Controllers
*/
final class UserValidationRulesFactory
{
/**
* @param array $data
* @param bool $update
* @return array
*/
public static function build(array $data, $update = false){
if($update){
return [
'first_name' => 'sometimes|string',
'last_name' => 'sometimes|string',
'email' => 'sometimes|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'gender_specify' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',
// admin fields
'email_verified' => 'nullable|boolean',
'active' => 'nullable|boolean',
'groups' => 'sometimes|int_array',
];
}
return [
'first_name' => 'required|string',
'last_name' => 'required|string',
'email' => 'required|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',
// admin fields
'email_verified' => 'nullable|boolean',
'active' => 'nullable|boolean',
'groups' => 'sometimes|int_array',
];
}
}

View File

@ -168,16 +168,19 @@ Route::group(['namespace' => 'App\Http\Controllers', 'middleware' => 'web' ], fu
'middleware' => ['ssl', 'auth']], function () {
Route::group(['prefix' => 'users'], function () {
Route::delete('/me/tokens/{value}',"UserApiController@revokeMyToken");
Route::get('' , "UserApiController@getAll");
Route::post('', ['middleware' => ['openstackid.currentuser.serveradmin.json'], 'uses' => "UserApiController@create"]);
Route::put('me', "UserApiController@updateMe");
Route::group(['prefix' => '{id}'], function(){
Route::group(['prefix' => 'locked'], function(){
Route::put('', ['middleware' => ['openstackid.currentuser.serveradmin.json'], 'uses' => 'UserApiController@unlock']);
Route::delete('', ['middleware' => ['openstackid.currentuser.serveradmin.json'], 'uses' => 'UserApiController@lock']);
});
Route::get('', ['middleware' => ['openstackid.currentuser.serveradmin.json'], 'uses' => "UserApiController@get"]);
Route::delete('', ['middleware' => ['openstackid.currentuser.serveradmin.json'], 'uses' =>"UserApiController@delete"]);
Route::put('', ['middleware' => ['openstackid.currentuser.serveradmin.json'], 'uses' =>"UserApiController@update"]);
@ -376,7 +379,15 @@ Route::group(
Route::group(['prefix' => 'users'], function () {
Route::get('', 'OAuth2UserApiController@getAll');
Route::get('/{id}', 'OAuth2UserApiController@get');
Route::get('/me', 'OAuth2UserApiController@me');
Route::group(['prefix' => 'me'], function () {
Route::get('', 'OAuth2UserApiController@me');
Route::put('','OAuth2UserApiController@UpdateMe');
Route::group(['prefix' => 'pic'], function () {
Route::put('','OAuth2UserApiController@UpdateMyPic');
});
});
Route::get('/info', 'OAuth2UserApiController@userInfo');
Route::post('/info', 'OAuth2UserApiController@userInfo');
});

View File

@ -90,7 +90,7 @@ class AppServiceProvider extends ServiceProvider
if(!is_array($value)) return false;
foreach($value as $element)
{
if(!is_int($element)) return false;
if(!is_integer($element)) return false;
}
return true;
});

View File

@ -12,6 +12,7 @@
* limitations under the License.
**/
use App\Events\UserEmailUpdated;
use App\Events\UserPasswordResetSuccessful;
use App\Jobs\PublishUserDeleted;
use App\Jobs\PublishUserUpdated;
use App\libs\Auth\Factories\UserFactory;
@ -229,11 +230,14 @@ final class UserService extends AbstractService implements IUserService
public function update(int $id, array $payload): IEntity
{
return $this->tx_service->transaction(function() use($id, $payload){
$user = $this->repository->getById($id);
if(is_null($user) || !$user instanceof User)
throw new EntityNotFoundException("user not found");
$former_email = $user->getEmail();
$former_password = $user->getPassword();
if(isset($payload["email"])){
$former_user = $this->repository->getByEmailOrName(trim($payload["email"]));
if(!is_null($former_user) && $former_user->getId() != $id)
@ -259,11 +263,16 @@ final class UserService extends AbstractService implements IUserService
}
if($former_email != $user->getEmail()){
Log::debug(sprintf("UserService::update use id %s - email changed old %s - email new %s", $id, $former_email , $user->getEmail()));
Log::warning(sprintf("UserService::update use id %s - email changed old %s - email new %s", $id, $former_email , $user->getEmail()));
$user->clearEmailVerification();
Event::fire(new UserEmailUpdated($user->getId()));
}
if($former_password != $user->getPassword()){
Log::warning(sprintf("UserService::update use id %s - password changed", $id));
Event::fire(new UserPasswordResetSuccessful($user->getId()));
}
try {
if(Config::get("queue.enable_message_broker", false) == true)
PublishUserUpdated::dispatch($user)->onConnection('message_broker');

View File

@ -26,5 +26,6 @@ interface IUserScopes
const Registration = 'user-registration';
const ReadAll = 'users-read-all';
const SSO = 'sso';
const MeRead = 'me/read';
const MeWrite = 'me/write';
}

View File

@ -0,0 +1,83 @@
<?php namespace Database\Migrations;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\libs\OAuth2\IUserScopes;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;
use SeedUtils;
/**
* Class Version20200811151509
* @package Database\Migrations
*/
class Version20200811151509 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
SeedUtils::seedScopes([
[
'name' => IUserScopes::MeRead,
'short_description' => 'Allows access to read your Profile',
'description' => 'Allows access to read your Profile',
'system' => false,
'default' => false,
'groups' => false,
],
], 'users');
SeedUtils::seedScopes([
[
'name' => IUserScopes::MeWrite,
'short_description' => 'Allows access to write your Profile',
'description' => 'Allows access to write your Profile',
'system' => false,
'default' => false,
'groups' => false,
],
], 'users');
SeedUtils::seedApiEndpoints('users', [
[
'name' => 'update-my-user',
'active' => true,
'route' => '/api/v1/users/me',
'http_method' => 'PUT',
'scopes' => [
\App\libs\OAuth2\IUserScopes::MeWrite
],
],
[
'name' => 'update-my-user-pic',
'active' => true,
'route' => '/api/v1/users/me/pic',
'http_method' => 'PUT',
'scopes' => [
\App\libs\OAuth2\IUserScopes::MeWrite
],
],
]);
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
}
}

View File

@ -83,7 +83,25 @@ class ApiEndpointSeeder extends Seeder
'scopes' => [
\App\libs\OAuth2\IUserScopes::ReadAll
],
]
],
[
'name' => 'update-my-user',
'active' => true,
'route' => '/api/v1/users/me',
'http_method' => 'PUT',
'scopes' => [
\App\libs\OAuth2\IUserScopes::MeWrite
],
],
[
'name' => 'update-my-user-pic',
'active' => true,
'route' => '/api/v1/users/me/pic',
'http_method' => 'PUT',
'scopes' => [
\App\libs\OAuth2\IUserScopes::MeWrite
],
],
]
);
}

View File

@ -65,6 +65,22 @@ class ApiScopeSeeder extends Seeder {
'system' => false,
'default' => false,
'groups' => true,
],
[
'name' => IUserScopes::MeRead,
'short_description' => 'Allows access to read your Profile',
'description' => 'Allows access to read your Profile',
'system' => false,
'default' => false,
'groups' => false,
],
[
'name' => IUserScopes::MeWrite,
'short_description' => 'Allows access to write your Profile',
'description' => 'Allows access to write your Profile',
'system' => false,
'default' => false,
'groups' => false,
]
], 'users');

View File

@ -1373,14 +1373,32 @@ PPK;
'system' => false,
'active' => true,
),
array(
[
'name' => 'address',
'short_description' => 'This scope value requests access to the address Claim.',
'description' => 'This scope value requests access to the address Claim.',
'api' => $api,
'system' => false,
'active' => true,
)
],
[
'name' => IUserScopes::MeRead,
'short_description' => 'Allows access to read your Profile',
'description' => 'Allows access to read your Profile',
'api' => $api,
'system' => false,
'default' => false,
'active' => true,
],
[
'name' => IUserScopes::MeWrite,
'short_description' => 'Allows access to write your Profile',
'description' => 'Allows access to write your Profile',
'api' => $api,
'system' => false,
'default' => false,
'active' => true,
]
];
foreach($api_scope_payloads as $payload) {
@ -1645,28 +1663,41 @@ PPK;
$users = $api_repository->findOneBy(['name' => 'users']);
$api_scope_payloads = [
array(
[
'name' => 'get-user-info',
'active' => true,
'api' => $users,
'route' => '/api/v1/users/me',
'http_method' => 'GET'
),
array(
],
[
'name' => 'get-user-claims-get',
'active' => true,
'api' => $users,
'route' => '/api/v1/users/info',
'http_method' => 'GET'
),
array(
],
[
'name' => 'get-user-claims-post',
'active' => true,
'api' => $users,
'route' => '/api/v1/users/info',
'http_method' => 'POST'
)
],
[
'name' => 'update-my-user',
'active' => true,
'route' => '/api/v1/users/me',
'api' => $users,
'http_method' => 'PUT',
],
[
'name' => 'update-my-user-pic',
'active' => true,
'route' => '/api/v1/users/me/pic',
'api' => $users,
'http_method' => 'PUT',
],
];
foreach($api_scope_payloads as $payload) {
@ -1678,12 +1709,14 @@ PPK;
$profile_scope = $api_scope_repository->findOneBy(['name' => 'profile']);
$email_scope = $api_scope_repository->findOneBy(['name' => 'email']);
$address_scope = $api_scope_repository->findOneBy(['name' => 'address']);
$me_write = $api_scope_repository->findOneBy(['name' => IUserScopes::MeWrite]);
foreach($api_scope_payloads as $payload) {
$endpoint = $endpoint_repository->findOneBy(['name' => $payload['name']]);
$endpoint->addScope($address_scope);
$endpoint->addScope($email_scope);
$endpoint->addScope($profile_scope);
$endpoint->addScope($me_write);
EntityManager::persist($endpoint);
}

View File

@ -11,13 +11,50 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\libs\OAuth2\IUserScopes;
use OAuth2\ResourceServer\IUserService;
/**
* Class OAuth2UserServiceApiTest
*/
final class OAuth2UserServiceApiTest extends OAuth2ProtectedApiTest {
/**
* @covers OAuth2UserApiController::get()
*/
public function testUpdateMe(){
$first_name_val = 'test_'. str_random(16);
$data = [
'first_name' => $first_name_val,
];
$params = [
];
$headers = [
"HTTP_Authorization" => " Bearer " . $this->access_token,
"CONTENT_TYPE" => "application/json"
];
$response = $this->action
(
"PUT",
"Api\\OAuth2\\OAuth2UserApiController@UpdateMe",
$params,
[],
[],
[],
$headers,
json_encode($data)
);
$this->assertResponseStatus(201);
$content = $response->getContent();
$user = json_decode($content);
$this->assertTrue($user->first_name == $first_name_val);
}
/**
* @covers OAuth2UserApiController::get()
@ -58,7 +95,8 @@ final class OAuth2UserServiceApiTest extends OAuth2ProtectedApiTest {
$scope = array(
IUserService::UserProfileScope_Address,
IUserService::UserProfileScope_Email,
IUserService::UserProfileScope_Profile
IUserService::UserProfileScope_Profile,
IUserScopes::MeWrite,
);
return $scope;