diff --git a/.env.testing b/.env.testing new file mode 100755 index 0000000..0512e04 --- /dev/null +++ b/.env.testing @@ -0,0 +1,36 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY=base64:FwpumMyUcvG3z+MSQjaXBoJydVKC24+QqzN2VZxzJSc= +APP_DEBUG=true +APP_URL=http://localhost:8080 + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=testing +DB_USERNAME=sail +DB_PASSWORD=password + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +L5_SWAGGER_CONST_HOST=${APP_URL}/api diff --git a/ANSWER.md b/ANSWER.md new file mode 100755 index 0000000..b0f3d9d --- /dev/null +++ b/ANSWER.md @@ -0,0 +1,29 @@ +# Eleven Soft Backend Refactoring Test Answers + + Adicionado o Dockerfile para subir o ambiente através de containers docker + + Adicionado o arquivo .env.testing para a base de dados de teste + + No Arquivo README.md nos itens Instalation and Setup e Documentation o endereço esta com a porta errada ao invés de 8000 é 8080 + + Criado a pasta Api/v1 dentro de Controllers para criar tipo um versionamento das apis. + + Utilização do Laravel Passport para garantir maior segurança nas operações de Crud, permitindo que somente usuários devidamente logados possam inserir, recuperar, atualizarinserir, atualizar, deleta + + Utiliização de RepositoryPattern para separar lógica de acesso a dados e a lógica da camada de negócio. + + Utilização de FormRequests no Crud para validar os dados enviados pela requisição antes de processa-los, dentre os beneficios estão separação de responsabilidades, centralização das regras de validações, melhor segurança + + Método Index retorna a listagem de todos os usuários cadastrados, por esse motivo foi adicionada paginação para evitar possível sobrecarregamento da API, podendo causar um timeout + + Pensando na gestão dos dados do usuário, e que o mesmo poderá apenas alterar a senha, no update de usuários, não será permitido alterar a senha, a alteração da senha será feita em uma api especifica para alteração de senha. + + Utilização de Resources para estruturar e formatar os dados da Model a serem retornados + + No Método delete, foi adicionado a trait de Softdeletes, e adicionado a coluna deleted_at na migration, para evitar a exclusão fisica do usuário e apenas inativá-lo, pois para exclusão do usuário, caso tenha dados em outra tabela associado a ele, estes dados também deverão ser excluídos, e podem ser dados sensíveis que não poderam ser excluídos. + + Adicionado Testes Unitários e de Integração para garantir que o código alterado não afetem o comportamento da aplicação + + Adicionado blocos try catch para tratamento de erros na controller + Adicionado Códigos HTTP de acordo com as respostas das solicitações, + 201 para inserção, 200 para lista, edição e atulização e 500 para erros no bloco Catch diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb65786 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +FROM ubuntu:22.04 + +LABEL maintainer="Taylor Otwell" + +ARG WWWGROUP + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + + +RUN apt-get update \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ + && mkdir -p ~/.gnupg \ + && chmod 600 ~/.gnupg \ + && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ + && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E5267A6C \ + && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C300EE8C \ + && echo "deb http://ppa.launchpad.net/ondrej/php/ubuntu focal main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ + && apt-get update \ + && apt-get install -y php82-cli php82-dev \ + php82-pgsql php82-sqlite3 php82-gd \ + php82-curl php82-memcached \ + php82-imap php82-mysql php82-mbstring \ + php82-xml php82-zip php82-bcmath php82-soap \ + php82-intl php82-readline \ + php82-msgpack php82-igbinary php82-ldap \ + php82-redis php82-mbstring \ + && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ + && curl -sL https://deb.nodesource.com/setup_16.x | bash - \ + && apt-get install -y nodejs \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get install -y yarn \ + && apt-get install -y mysql-client \ + && apt-get install -y postgresql-client \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + + + +# RUN pecl channel-update https://pecl.php.net/channel.xml \ +# && pecl install swoole \ +# && pecl clear-cache \ +# && rm -rf /tmp/* /var/tmp/* + + + + +RUN setcap "cap_net_bind_service=+ep" /usr/bin/php82 + +RUN useradd -m sail +RUN usermod -aG sudo sail + +# RUN groupadd --force -g $WWWGROUP sail +# RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +RUN curl -fsSL https://deb.nodesource.com/setup_12.x | bash - +RUN apt-get install -y nodejs + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + + +COPY . /var/www/html/ +COPY resources/docker/app/start-container /usr/local/bin/start-container +COPY resources/docker/app/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY resources/docker/app/php.ini /etc/php/82/cli/conf.d/99-sail.ini +RUN chmod +x /usr/local/bin/start-container + +EXPOSE 80 + +ENTRYPOINT ["start-container"] diff --git a/app/Http/Controllers/Api/v1/Auth/AuthController.php b/app/Http/Controllers/Api/v1/Auth/AuthController.php new file mode 100755 index 0000000..9bc9efb --- /dev/null +++ b/app/Http/Controllers/Api/v1/Auth/AuthController.php @@ -0,0 +1,113 @@ + $request->email, + 'password' => $request->password + ]; + + if (auth()->attempt($data)) { + $token = auth()->user()->createToken('LaravelAuthApp')->accessToken; + $data = [ + 'message' => 'Login efetuado com sucesso', + 'user' => Auth::user(), + 'token' => $token + ]; + return response()->json($data, JsonResponse::HTTP_OK); + } else { + return response()->json(['error' => 'Unauthorised'], JsonResponse::HTTP_UNAUTHORIZED); + } + } + + /** + * Make a login + * + * @return JsonResponse + * + * @OA\Get( + * path="/v1/auth/me", + * operationId="Get User logged data", + * summary="Return infos of user logged", + * tags={"Auth"}, + * description="Get User logged data", + * security={ + * {"bearerAuth": {}} + * }, + * @OA\Response( + * response=200, + * description="Successful operation", + * @OA\JsonContent(ref="#/components/schemas/User") + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ) + * ) + */ + public function me(): JsonResponse + { + return response()->json(['user' => Auth::user()], JsonResponse::HTTP_OK); + } + + public function logout(Request $request): JsonResponse + { + $request->user()->tokens()->delete(); + + return response()->json(['Logout efetuado com sucesso'], JsonResponse::HTTP_OK); + } +} diff --git a/app/Http/Controllers/Api/v1/User/UserController.php b/app/Http/Controllers/Api/v1/User/UserController.php new file mode 100755 index 0000000..65af7ac --- /dev/null +++ b/app/Http/Controllers/Api/v1/User/UserController.php @@ -0,0 +1,361 @@ +has('perPage') ? $request->per_page : 20; + $page = $request->has('page') ? $request->page : 1; + + $users = UserResource::collection($this->userRepository->getAllUsersPaginate($perPage, $page)); + return response()->json([ + 'data' => $users, + ], JsonResponse::HTTP_OK); + } catch (\Exception $e) { + return response()->json([ + 'data' => [], + 'errorMessage' => $e->getMessage(), + 'errorCode' => $e->getCode(), + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Show a specific user resource + * + * @return User + * + * @OA\Get( + * path="/users/{id}", + * operationId="showUser", + * summary="Show a specific user", + * tags={"Users"}, + * description="Returns a specific user", + * security={ + * {"bearerAuth": {}} + * }, + * @OA\Parameter( + * name="id", + * description="User ID", + * required=true, + * in="path", + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * @OA\JsonContent(ref="#/components/schemas/User") + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ) + * ) + */ + public function show($id) + { + try { + $users = new UserResource($this->userRepository->getUserById($id)); + return response()->json([ + 'data' => $users, + ], JsonResponse::HTTP_OK); + } catch (\Exception $e) { + return response()->json([ + 'data' => [], + 'errorMessage' => $e->getMessage(), + 'errorCode' => $e->getCode(), + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Store a newly created user in storage. + * + * @return JsonResponse + * + * @OA\Post( + * path="/users", + * operationId="storeUser", + * summary="Store a new user", + * tags={"Users"}, + * description="Stores a new user", + * security={ + * {"bearerAuth": {}} + * }, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent(ref="#/components/schemas/User") + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * @OA\JsonContent(ref="#/components/schemas/User") + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ) + * ) + */ + public function store(UserStoreRequest $request): JsonResponse + { + try { + $user = new UserResource($this->userRepository->createUser($request->all())); + + return response()->json([ + 'message' => 'Usuario inserido com sucesso', + 'data' => $user, + ], JsonResponse::HTTP_CREATED); + } catch (\Exception $e) { + return response()->json([ + 'data' => [], + 'errorMessage' => $e->getMessage(), + 'errorCode' => $e->getCode(), + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Update a specific user resource + * + * @return User + * + * @OA\Put( + * path="/users/{id}", + * operationId="updateUser", + * summary="Update a specific user", + * tags={"Users"}, + * description="Updates a specific user", + * security={ + * {"bearerAuth": {}} + * }, + * @OA\Parameter( + * name="id", + * description="User ID", + * required=true, + * in="path", + * ), + * @OA\RequestBody( + * required=true, + * @OA\JsonContent(ref="#/components/schemas/User") + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * @OA\JsonContent(ref="#/components/schemas/User") + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ) + * ) + */ + public function update(UserUpdateRequest $request, $id) + { + try { + + $data = $request->only([ + 'name', + 'email', + 'password', + ]); + + $this->userRepository->updateUser($id, $data); + $user = new UserResource($this->userRepository->getUserById($id)); + + return response()->json([ + 'message' => "Usuário Alterado com sucesso", + 'data' => $user, + ], JsonResponse::HTTP_OK); + } catch (\Exception $e) { + return response()->json([ + 'data' => [], + 'errorMessage' => $e->getMessage(), + 'errorCode' => $e->getCode(), + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Remove a specific user resource + * + * @return User + * + * @OA\Delete( + * path="/users/{id}", + * operationId="deleteUser", + * summary="Delete a specific user", + * tags={"Users"}, + * description="Deletes a specific user", + * security={ + * {"bearerAuth": {}} + * }, + * @OA\Parameter( + * name="id", + * description="User ID", + * required=true, + * in="path", + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * @OA\JsonContent(ref="#/components/schemas/User") + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ) + * ) + */ + public function destroy($id) + { + try { + $this->userRepository->deleteUser($id); + + return response()->json([ + 'message' => 'Usuario excluido com sucesso' + ], JsonResponse::HTTP_OK); + } catch (\Exception $e) { + return response()->json([ + 'data' => [], + 'errorMessage' => $e->getMessage(), + 'errorCode' => $e->getCode(), + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Make a login + * + * @return JsonResponse + * + * @OA\Put( + * path="/v1/users/alterar-senha/{id}", + * operationId="updatePassword", + * summary="Update password from user", + * tags={"Users"}, + * description="Update password from user", + * security={ + * {"bearerAuth": {}} + * }, + * @OA\Parameter( + * name="id", + * description="User ID", + * required=true, + * in="path", + * ), + * @OA\RequestBody( + * required=true, + * description="Provide All Info Below", + * @OA\JsonContent( + * required={"email","password"}, + * @OA\Property(property="password", type="string", format="text", example="password"), + * ), + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * @OA\JsonContent(ref="#/components/schemas/User") + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ) + * ) + */ + public function updatePassword(UserPasswordUpdateRequest $request, $id) { + try { + + $password = bcrypt($request->password); + $this->userRepository->updateUser($id, array('password' => $password)); + $user = new UserResource($this->userRepository->getUserById($id)); + + return response()->json([ + 'message' => "Senha Alterada com sucesso", + 'data' => $user, + ], JsonResponse::HTTP_OK); + } catch (\Exception $e) { + return response()->json([ + 'data' => [], + 'errorMessage' => $e->getMessage(), + 'errorCode' => $e->getCode(), + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } +} + diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php old mode 100644 new mode 100755 diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php deleted file mode 100644 index f4a5826..0000000 --- a/app/Http/Controllers/UserController.php +++ /dev/null @@ -1,234 +0,0 @@ -user = $user; - } - - /** - * Display a listing of the resource. - * - * @return Response - * - * @OA\Get( - * path="/users", - * operationId="getUsersList", - * summary="Get list of users", - * tags={"Users"}, - * description="Returns list of users", - * security={ - * {"bearerAuth": {}} - * }, - * @OA\Response( - * response=200, - * description="Successful operation", - * @OA\JsonContent( - * type="array", - * @OA\Items( - * ref="#/components/schemas/User" - * ) - * ), - * ), - * @OA\Response( - * response=401, - * description="Unauthenticated", - * ), - * @OA\Response( - * response=403, - * description="Forbidden" - * ) - * ) - */ - public function index(Request $request) - { - return $this->user->get(); - } - - /** - * Show a specific user resource - * - * @return User - * - * @OA\Get( - * path="/users/{id}", - * operationId="showUser", - * summary="Show a specific user", - * tags={"Users"}, - * description="Returns a specific user", - * security={ - * {"bearerAuth": {}} - * }, - * @OA\Parameter( - * name="id", - * description="User ID", - * required=true, - * in="path", - * ), - * @OA\Response( - * response=200, - * description="Successful operation", - * @OA\JsonContent(ref="#/components/schemas/User") - * ), - * @OA\Response( - * response=401, - * description="Unauthenticated", - * ), - * @OA\Response( - * response=403, - * description="Forbidden" - * ) - * ) - */ - public function show(User $user) - { - return $user; - } - - /** - * Store a newly created user in storage. - * - * @return User - * - * @OA\Post( - * path="/users", - * operationId="storeUser", - * summary="Store a new user", - * tags={"Users"}, - * description="Stores a new user", - * security={ - * {"bearerAuth": {}} - * }, - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/User") - * ), - * @OA\Response( - * response=200, - * description="Successful operation", - * @OA\JsonContent(ref="#/components/schemas/User") - * ), - * @OA\Response( - * response=401, - * description="Unauthenticated", - * ), - * @OA\Response( - * response=403, - * description="Forbidden" - * ) - * ) - */ - public function store(Request $request) - { - $data = $request->only([ - 'name', - 'email', - 'password', - ]); - - return $this->user->create($data); - } - - /** - * Update a specific user resource - * - * @return User - * - * @OA\Put( - * path="/users/{id}", - * operationId="updateUser", - * summary="Update a specific user", - * tags={"Users"}, - * description="Updates a specific user", - * security={ - * {"bearerAuth": {}} - * }, - * @OA\Parameter( - * name="id", - * description="User ID", - * required=true, - * in="path", - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/User") - * ), - * @OA\Response( - * response=200, - * description="Successful operation", - * @OA\JsonContent(ref="#/components/schemas/User") - * ), - * @OA\Response( - * response=401, - * description="Unauthenticated", - * ), - * @OA\Response( - * response=403, - * description="Forbidden" - * ) - * ) - */ - public function update(Request $request, User $user) - { - $data = $request->only([ - 'name', - 'email', - 'password', - ]); - - $user->update($data); - - return $user; - } - - /** - * Remove a specific user resource - * - * @return User - * - * @OA\Delete( - * path="/users/{id}", - * operationId="deleteUser", - * summary="Delete a specific user", - * tags={"Users"}, - * description="Deletes a specific user", - * security={ - * {"bearerAuth": {}} - * }, - * @OA\Parameter( - * name="id", - * description="User ID", - * required=true, - * in="path", - * ), - * @OA\Response( - * response=200, - * description="Successful operation", - * @OA\JsonContent(ref="#/components/schemas/User") - * ), - * @OA\Response( - * response=401, - * description="Unauthenticated", - * ), - * @OA\Response( - * response=403, - * description="Forbidden" - * ) - * ) - */ - public function destroy(User $user) - { - $user->delete(); - - return $user; - } -} - diff --git a/app/Http/Requests/Auth/AuthFormRequest.php b/app/Http/Requests/Auth/AuthFormRequest.php new file mode 100644 index 0000000..839e86e --- /dev/null +++ b/app/Http/Requests/Auth/AuthFormRequest.php @@ -0,0 +1,54 @@ + + */ + public function rules() + { + return [ + 'email' => 'required|email', + 'password' => 'required|min:8' + ]; + } + + public function failedValidation(Validator $validator) + { + throw new HttpResponseException(response()->json([ + 'success' => false, + 'message' => 'Errors found', + 'data' => $validator->errors() + ])); + } + + public function messages() + { + return [ + 'email.required' => 'O Campo Email é obrigatório', + 'email.email' => 'O Campo Email possui um formato invalido', + 'password.required' => 'O Campo senha é obrigatório', + 'password.min' => 'O Campo senha deve conter no mínimo 8 caracteres' + ]; + } + +} diff --git a/app/Http/Requests/User/UserPasswordUpdateRequest.php b/app/Http/Requests/User/UserPasswordUpdateRequest.php new file mode 100644 index 0000000..4de7472 --- /dev/null +++ b/app/Http/Requests/User/UserPasswordUpdateRequest.php @@ -0,0 +1,50 @@ + + */ + public function rules() + { + + return [ + 'password' => 'required|min:8' + ]; + } + + public function failedValidation(Validator $validator) + { + throw new HttpResponseException(response()->json([ + 'success' => false, + 'message' => 'Errors found', + 'data' => $validator->errors() + ])); + } + + public function messages() + { + return [ + 'password.required' => 'O Campo senha é obrigatório', + 'password.min' => 'O Campo senha deve conter no mínimo 8 caracteres' + ]; + } +} diff --git a/app/Http/Requests/User/UserStoreRequest.php b/app/Http/Requests/User/UserStoreRequest.php new file mode 100644 index 0000000..d7ccb5e --- /dev/null +++ b/app/Http/Requests/User/UserStoreRequest.php @@ -0,0 +1,57 @@ + + */ + public function rules() + { + return [ + 'name' => 'required', + 'email' => 'required|email|unique:users,email', + 'password' => 'required|min:8' + ]; + } + + public function failedValidation(Validator $validator) + { + throw new HttpResponseException(response()->json([ + 'success' => false, + 'message' => 'Errors found', + 'data' => $validator->errors() + ])); + } + + public function messages() + { + return [ + 'name.required' => 'O Campo Nome é obrigatório', + 'email.required' => 'O Campo Email é obrigatório', + 'email.email' => 'O Campo Email possui um formato invalido', + 'email.unique' => 'Email já cadastrado', + 'password.required' => 'O Campo senha é obrigatório', + 'password.min' => 'O Campo senha deve conter no mínimo 8 caracteres' + ]; + } + +} diff --git a/app/Http/Requests/User/UserUpdateRequest.php b/app/Http/Requests/User/UserUpdateRequest.php new file mode 100644 index 0000000..cf731c4 --- /dev/null +++ b/app/Http/Requests/User/UserUpdateRequest.php @@ -0,0 +1,53 @@ + + */ + public function rules() + { + + return [ + 'name' => 'required', + 'email' => 'required|email|unique:users,email,' . $this->id . ',id' + ]; + } + + public function failedValidation(Validator $validator) + { + throw new HttpResponseException(response()->json([ + 'success' => false, + 'message' => 'Errors found', + 'data' => $validator->errors() + ])); + } + + public function messages() + { + return [ + 'name.required' => 'O Campo Nome é obrigatório', + 'email.required' => 'O Campo Email é obrigatório', + 'email.email' => 'O Campo Email possui um formato invalido', + 'email.unique' => 'Email já cadastrado' + ]; + } +} diff --git a/app/Http/Resources/User/UserResource.php b/app/Http/Resources/User/UserResource.php new file mode 100755 index 0000000..ef4f7d3 --- /dev/null +++ b/app/Http/Resources/User/UserResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'created_at' => $this->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), + ]; + } +} diff --git a/app/Interfaces/User/UserRepositoryInterface.php b/app/Interfaces/User/UserRepositoryInterface.php new file mode 100755 index 0000000..a17c7af --- /dev/null +++ b/app/Interfaces/User/UserRepositoryInterface.php @@ -0,0 +1,15 @@ +app->bind(UserRepositoryInterface::class, UserRepository::class); + + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/app/Repositories/User/UserRepository.php b/app/Repositories/User/UserRepository.php new file mode 100755 index 0000000..b097f82 --- /dev/null +++ b/app/Repositories/User/UserRepository.php @@ -0,0 +1,50 @@ +first(); + } + + public function searchUserByColumn($column, $value) { + + return User::where($column, 'LIKE', "%{$value}%")->get(); + } + + public function deleteUser($userId) + { + User::find($userId)->delete(); + } + + public function createUser(array $userDetails) + { + return User::create($userDetails); + } + + public function updateUser($userId, array $newDetails) + { + + return User::find($userId)->update($newDetails); + } +} diff --git a/config/app.php b/config/app.php old mode 100644 new mode 100755 index 1d662e6..e9675df --- a/config/app.php +++ b/config/app.php @@ -169,6 +169,8 @@ // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, + App\Providers\RepositoryServiceProvider::class, + Laravel\Passport\PassportServiceProvider::class, ])->toArray(), /* diff --git a/config/auth.php b/config/auth.php old mode 100644 new mode 100755 index 9548c15..f2db043 --- a/config/auth.php +++ b/config/auth.php @@ -40,6 +40,11 @@ 'driver' => 'session', 'provider' => 'users', ], + + 'api' => [ + 'driver' => 'passport', + 'provider' => 'users', + ], ], /* diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php old mode 100644 new mode 100755 index 444fafb..50c4f96 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -11,15 +11,19 @@ */ public function up(): void { - Schema::create('users', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); + if ( ! Schema::hasTable('users')) + { + Schema::create('users', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + $table->softDeletes(); + }); + } } /** diff --git a/database/seeders/UserTableSeeder.php b/database/seeders/UserTableSeeder.php index 00fec09..b51dd2a 100644 --- a/database/seeders/UserTableSeeder.php +++ b/database/seeders/UserTableSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; +use App\Models\User; class UserTableSeeder extends Seeder { @@ -11,7 +12,7 @@ class UserTableSeeder extends Seeder */ public function run(): void { - \App\Models\User::factory()->create([ + User::create([ 'name' => 'example', 'email' => 'example@elevensoft.dev', 'password' => bcrypt('password') diff --git a/makefile b/makefile old mode 100644 new mode 100755 index 5d94f95..f488c7a --- a/makefile +++ b/makefile @@ -4,19 +4,19 @@ sleep: sleep 3 ps: - docker-compose ps + docker compose ps up: - docker-compose up -d + docker compose up -d up-recreate: - docker-compose up -d --force-recreate + docker compose up -d --force-recreate down: - docker-compose down + docker compose down forget: - docker-compose down --rmi all --volumes + docker compose down --rmi all --volumes docker volume rm backend-test_sail-mysql 2>/dev/null db-shell: @@ -26,26 +26,26 @@ api-build: USER_ID=$(shell id -u) GROUP_ID=$(shell id -g) docker-compose build --no-cache api-db: - docker-compose exec -it api php /var/www/html/artisan migrate:fresh - docker-compose exec -it api php /var/www/html/artisan db:seed + docker compose exec -it api php /var/www/html/artisan migrate:fresh + docker compose exec -it api php /var/www/html/artisan db:seed api-key: - docker-compose exec -it api php /var/www/html/artisan key:generate + docker compose exec -it api php /var/www/html/artisan key:generate api-env: cp .env.example .env api-config-cache: - docker-compose exec -it api php /var/www/html/artisan config:cache + docker compose exec -it api php /var/www/html/artisan config:cache api-composer-install: composer install --ignore-platform-reqs api-shell: - docker-compose exec -it api bash -c 'su sail' + docker compose exec -it api bash -c 'su sail' api-root-shell: - docker-compose exec -it api bash + docker compose exec -it api bash api-test: docker-compose exec -it api php /var/www/html/artisan test @@ -54,17 +54,17 @@ api-test-feature: docker-compose exec -it api php /var/www/html/artisan test --testsuite=Feature --stop-on-failure api-test-php-unit: - docker-compose exec -it api php /var/www/html/artisan phpunit + docker compose exec -it api php /var/www/html/artisan phpunit api-build-swagger: - docker-compose exec -it api php /var/www/html/artisan l5-swagger:generate + docker compose exec -it api php /var/www/html/artisan l5-swagger:generate api-passport-key: - docker-compose exec -it api php /var/www/html/artisan passport:keys --force + docker compose exec -it api php /var/www/html/artisan passport:keys --force api-passport-generate: - docker-compose exec -it api php /var/www/html/artisan passport:client --password --name='Laravel Password Grant Client' --provider=users > .passport + docker compose exec -it api php /var/www/html/artisan passport:client --password --name='Laravel Password Grant Client' --provider=users > .passport cat .passport fix-permissions: - docker-compose exec -it api bash -c 'chmod -R 777 /var/www/html/storage/logs && chmod -R 777 /var/www/html/storage/framework' + docker compose exec -it api bash -c 'chmod -R 777 /var/www/html/storage/logs && chmod -R 777 /var/www/html/storage/framework' diff --git a/routes/api.php b/routes/api.php old mode 100644 new mode 100755 index 03a44e1..7cd9a17 --- a/routes/api.php +++ b/routes/api.php @@ -1,7 +1,8 @@ group(function() { + Route::get('/v1/auth/me', [AuthController::class, 'me']); + Route::post('/v1/auth/logout', [AuthController::class, 'logout']); + Route::put('/v1/users/alterar-senha/{id}', [UserController::class, 'updatePassword']); + Route::apiResource('users', UserController::class); +}); diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json old mode 100644 new mode 100755 index 244d983..d75f5ff --- a/storage/api-docs/api-docs.json +++ b/storage/api-docs/api-docs.json @@ -15,6 +15,94 @@ } ], "paths": { + "/v1/auth/login": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Autehticate with valid user", + "description": "Make a Login", + "operationId": "Login", + "requestBody": { + "description": "Provide All Info Below", + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "email", + "format": "text", + "example": "example@elevensoft.dev" + }, + "password": { + "type": "string", + "format": "text", + "example": "password" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "401": { + "description": "Unauthenticated" + }, + "403": { + "description": "Forbidden" + } + } + } + }, + "/v1/auth/me": { + "get": { + "tags": [ + "Auth" + ], + "summary": "Return infos of user logged", + "description": "Get User logged data", + "operationId": "Get User logged data", + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "401": { + "description": "Unauthenticated" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/users": { "get": { "tags": [ @@ -220,6 +308,69 @@ } ] } + }, + "/v1/users/alterar-senha/{id}": { + "put": { + "tags": [ + "Users" + ], + "summary": "Update password from user", + "description": "Update password from user", + "operationId": "updatePassword", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "User ID", + "required": true + } + ], + "requestBody": { + "description": "Provide All Info Below", + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "email", + "password" + ], + "properties": { + "password": { + "type": "string", + "format": "text", + "example": "password" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "401": { + "description": "Unauthenticated" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, "components": { @@ -301,5 +452,15 @@ } } } - } + }, + "tags": [ + { + "name": "Auth", + "description": "Auth" + }, + { + "name": "Users", + "description": "Users" + } + ] } \ No newline at end of file diff --git a/storage/app/.gitignore b/storage/app/.gitignore old mode 100644 new mode 100755 diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore old mode 100644 new mode 100755 diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore old mode 100644 new mode 100755 diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php old mode 100644 new mode 100755 diff --git a/tests/Feature/Auth/AuthControllerTest.php b/tests/Feature/Auth/AuthControllerTest.php new file mode 100755 index 0000000..57e2f3b --- /dev/null +++ b/tests/Feature/Auth/AuthControllerTest.php @@ -0,0 +1,58 @@ + 'example@elevensoft.dev', + 'password' => 'password' + ]; + + $response = $this->postJson('/api/v1/auth/login', $userData); + + $response->assertOK(); + $response->assertJsonStructure([ + 'message', + 'user' => [ + 'id', + 'name', + 'email', + 'email_verified_at', + 'created_at', + 'updated_at' + + ], + 'token' + ]); + + } + + public function testCannotMakeloginWithInvalidCredentials(): void + { + + $userData = [ + 'email' => 'admin@teste.com', + 'password' => 'admin@teste' + ]; + + $response = $this->postJson('/api/v1/auth/login', $userData); + + $response + ->assertUnauthorized(); + + } + + public function testCanMakeLogout(): void + { + $headers = $this->makeAuth(); + $response = $this->postJson('/api/v1/auth/logout', [], $headers); + $response->assertOK(); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php old mode 100644 new mode 100755 diff --git a/tests/Feature/User/UserControllerTest.php b/tests/Feature/User/UserControllerTest.php new file mode 100755 index 0000000..1461f95 --- /dev/null +++ b/tests/Feature/User/UserControllerTest.php @@ -0,0 +1,124 @@ +raw(); + $header = $this->makeAuth(); + + $response = $this->postJson('/api/users', $userData, $header); + + $response + ->assertCreated() + ->assertJsonStructure([ + 'message', + 'data' => [ + 'id', + 'name', + 'email', + 'created_at', + 'updated_at' + ] + ]); + + } + + public function testWithoutAuthorizationCannotStoreUser(): void + { + $userData = User::factory()->raw(); + $header = $this->makeUnauthorizedAuth(); + + $response = $this->postJson('/api/users', $userData, $header); + + $response + ->assertUnauthorized(); + + } + + public function testCanListUsers(): void + { + + $header = $this->makeAuth(); + $response = $this->getJson('/api/users', $header); + + $response->assertOK(); + + } + + public function testCanEditUser(): void + { + $header = $this->makeAuth(); + $user = User::factory()->create(); + + $response = $this->getJson('/api/users/' . $user['id'], $header); + + $response->assertOk(); + $response->assertJsonCount(1); + $response->assertJsonStructure([ + 'data' => [ + 'id', + 'name', + 'email', + 'created_at', + 'updated_at' + ] + ]); + + } + + public function testCanDeleteUser(): void + { + $header = $this->makeAuth(); + $user = User::factory()->create(); + + $response = $this->deleteJson('/api/users/' . $user['id'], [], $header); + + $response->assertOk()->assertJsonCount(1); + + } + + public function testCanUpdateUser(): void + { + $header = $this->makeAuth(); + $user = User::factory()->create(); + $data = [ + "name" => fake()->name(), + "email" => fake()->email() + ]; + $response = $this->putJson('/api/users/' . $user['id'], $data, $header); + + $response->assertOk()->assertJsonCount(2); + + } + + public function testCanUpdatePassword(): void + { + $header = $this->makeAuth(); + $user = User::factory()->create(); + + $data = [ + "password" => fake()->password() + ]; + + $response = $this->putJson('/api/v1/users/alterar-senha/' . $user['id'], $data, $header); + + $response->assertOk()->assertJsonCount(2); + + } + +} diff --git a/tests/TestCase.php b/tests/TestCase.php old mode 100644 new mode 100755 index 2932d4a..c79d03d --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,4 +7,37 @@ abstract class TestCase extends BaseTestCase { use CreatesApplication; + + public function makeAuth(): array + { + $userData = [ + 'email' => 'example@elevensoft.dev', + 'password' => 'password' + ]; + + $response = $this->postJson('/api/v1/auth/login', $userData); + $data = json_decode($response->getContent()); + + $headers = [ + 'Authorization' => 'Bearer ' . $data->token + ]; + + return $headers; + } + + + public function makeUnauthorizedAuth(): array + { + $userData = [ + 'email' => 'example@elevensoft.dev', + 'password' => 'teste' + ]; + + $response = $this->postJson('/api/v1/auth/login', $userData); + $data = json_decode($response->getContent()); + + $headers = []; + + return $headers; + } } diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php old mode 100644 new mode 100755 diff --git a/tests/Unit/Models/User/UserModelTest.php b/tests/Unit/Models/User/UserModelTest.php new file mode 100755 index 0000000..6345554 --- /dev/null +++ b/tests/Unit/Models/User/UserModelTest.php @@ -0,0 +1,75 @@ +raw(); + $user = User::create($userFactory); + $arrUser = json_decode($user,true); + + $this->assertArrayHasKey('id', $arrUser); + $this->assertArrayHasKey('name', $arrUser); + $this->assertArrayHasKey('email', $arrUser); + $this->assertEquals(5, count($arrUser)); + + } + + public function testForUserEditById(): void + { + $userFactory = User::factory()->raw(); + $userIns = User::create($userFactory); + $user = User::where('email', $userIns['email'])->first(); + $arrUser = json_decode($user,true); + + $this->assertArrayHasKey('id', $arrUser); + $this->assertArrayHasKey('name', $arrUser); + $this->assertArrayHasKey('email', $arrUser); + $this->assertEquals(7, count($arrUser)); + + } + + public function testForUserUpdate(): void + { + $userFactory = User::factory()->raw(); + + $userIns = User::create($userFactory); + $arrUserIns = json_decode($userIns,true); + + $user = User::find($arrUserIns['id']); + $arrUserIns['name'] = $userIns['name'] . ' - Update'; + unset($arrUserIns['id']); + $user->update($arrUserIns); + + $arrUser = json_decode($user,true); + + $this->assertArrayHasKey('id', $arrUser); + $this->assertArrayHasKey('name', $arrUser); + $this->assertArrayHasKey('email', $arrUser); + $this->assertEquals(7, count($arrUser)); + + } + + public function testForUserDelete(): void + { + $userFactory = User::factory()->raw(); + + $userIns = User::create($userFactory); + $arrUserIns = json_decode($userIns,true); + + $user = User::find($arrUserIns['id']); + $value = $user->delete(); + + $this->assertNotNull($value); + $this->assertTrue($value); + + } + +} diff --git a/tests/Unit/Requests/Auth/AuthFormRequestTest.php b/tests/Unit/Requests/Auth/AuthFormRequestTest.php new file mode 100755 index 0000000..721c74f --- /dev/null +++ b/tests/Unit/Requests/Auth/AuthFormRequestTest.php @@ -0,0 +1,51 @@ +rules = (new AuthFormRequest())->rules(); + $this->validator = $this->app['validator']; + } + + public function testForAuthIsValidEmail(): void + { + + $this->assertTrue($this->validateField('email', 'usern@elevensoft.dev')); + $this->assertTrue($this->validateField('email', 'usern@elevensoft')); + $this->assertFalse($this->validateField('email', 'userelevensoft.com')); + $this->assertFalse($this->validateField('email', '')); + + } + + public function testForAuthIsValidPassword(): void + { + + $this->assertTrue($this->validateField('password', 'inf41234')); + $this->assertFalse($this->validateField('password', 'inf123')); + $this->assertFalse($this->validateField('email', '')); + + } + + protected function validateField(string $field, $value): bool + { + return $this->validator->make( + [$field => $value], + [$field => $this->rules[$field]] + )->passes(); + } +} diff --git a/tests/Unit/Requests/User/UserStoreRequestTest.php b/tests/Unit/Requests/User/UserStoreRequestTest.php new file mode 100755 index 0000000..41df879 --- /dev/null +++ b/tests/Unit/Requests/User/UserStoreRequestTest.php @@ -0,0 +1,63 @@ +rules = (new UserStoreRequest())->rules(); + $this->validator = $this->app['validator']; + } + + public function testForUserIsValidName(): void + { + + $this->assertTrue($this->validateField('name', fake()->name())); + $this->assertFalse($this->validateField('name', '')); + + } + + public function testForUserIsValidEmail(): void + { + + $this->assertTrue($this->validateField('email', fake()->email())); + $this->assertTrue($this->validateField('email', 'user@elevensoft')); + $this->assertFalse($this->validateField('email', 'userelevensoft.com')); + $this->assertFalse($this->validateField('email', 'example@elevensoft.dev')); + $this->assertFalse($this->validateField('email', '')); + + } + + public function testForUserIsValidPassword(): void + { + + $this->assertTrue($this->validateField('password', Str::random())); + $this->assertFalse($this->validateField('password', Str::random(7))); + $this->assertFalse($this->validateField('password', '')); + + } + + protected function validateField(string $field, $value): bool + { + return $this->validator->make( + [$field => $value], + [$field => $this->rules[$field]] + )->passes(); + } +}