├── .editorconfig ├── .gitignore ├── .styleci.yml ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── backend ├── .env.example ├── .env.testing ├── .env.travis ├── .gitignore ├── Procfile ├── README.md ├── app │ ├── Action │ │ ├── Auth │ │ │ ├── AuthenticationResponse.php │ │ │ ├── GetAuthenticatedUserAction.php │ │ │ ├── GetAuthenticatedUserResponse.php │ │ │ ├── LoginAction.php │ │ │ ├── LoginRequest.php │ │ │ ├── LogoutAction.php │ │ │ ├── RegisterAction.php │ │ │ ├── RegisterRequest.php │ │ │ ├── UpdateProfileAction.php │ │ │ ├── UpdateProfileRequest.php │ │ │ ├── UpdateProfileResponse.php │ │ │ ├── UploadProfileImageAction.php │ │ │ ├── UploadProfileImageRequest.php │ │ │ └── UploadProfileImageResponse.php │ │ ├── Comment │ │ │ ├── AddCommentAction.php │ │ │ ├── AddCommentRequest.php │ │ │ ├── AddCommentResponse.php │ │ │ ├── GetCommentByIdAction.php │ │ │ ├── GetCommentByIdResponse.php │ │ │ ├── GetCommentCollectionAction.php │ │ │ ├── GetCommentCollectionByTweetIdAction.php │ │ │ └── GetCommentCollectionByTweetIdRequest.php │ │ ├── GetByIdRequest.php │ │ ├── GetCollectionRequest.php │ │ ├── PaginatedResponse.php │ │ ├── Tweet │ │ │ ├── AddTweetAction.php │ │ │ ├── AddTweetRequest.php │ │ │ ├── AddTweetResponse.php │ │ │ ├── DeleteTweetAction.php │ │ │ ├── DeleteTweetRequest.php │ │ │ ├── GetTweetByIdAction.php │ │ │ ├── GetTweetByIdResponse.php │ │ │ ├── GetTweetCollectionAction.php │ │ │ ├── GetTweetCollectionByUserIdAction.php │ │ │ ├── GetTweetCollectionByUserIdRequest.php │ │ │ ├── LikeTweetAction.php │ │ │ ├── LikeTweetRequest.php │ │ │ ├── LikeTweetResponse.php │ │ │ ├── UpdateTweetAction.php │ │ │ ├── UpdateTweetRequest.php │ │ │ ├── UpdateTweetResponse.php │ │ │ ├── UploadTweetImageAction.php │ │ │ ├── UploadTweetImageRequest.php │ │ │ └── UploadTweetImageResponse.php │ │ └── User │ │ │ ├── GetUserByIdAction.php │ │ │ ├── GetUserByIdResponse.php │ │ │ └── GetUserCollectionAction.php │ ├── Console │ │ └── Kernel.php │ ├── Events │ │ └── TweetAddedEvent.php │ ├── Exceptions │ │ ├── ErrorCode.php │ │ ├── Handler.php │ │ ├── TweetNotFoundException.php │ │ └── UserNotFoundException.php │ ├── Http │ │ ├── Controllers │ │ │ ├── Api │ │ │ │ ├── Auth │ │ │ │ │ └── AuthController.php │ │ │ │ ├── CommentController.php │ │ │ │ ├── LikeController.php │ │ │ │ ├── TweetController.php │ │ │ │ └── UserController.php │ │ │ ├── ApiController.php │ │ │ ├── Auth │ │ │ │ ├── ConfirmPasswordController.php │ │ │ │ ├── ForgotPasswordController.php │ │ │ │ ├── LoginController.php │ │ │ │ ├── RegisterController.php │ │ │ │ ├── ResetPasswordController.php │ │ │ │ └── VerificationController.php │ │ │ └── Controller.php │ │ ├── Kernel.php │ │ ├── Middleware │ │ │ ├── Authenticate.php │ │ │ ├── EncryptCookies.php │ │ │ ├── PreventRequestsDuringMaintenance.php │ │ │ ├── RedirectIfAuthenticated.php │ │ │ ├── TrimStrings.php │ │ │ ├── TrustHosts.php │ │ │ ├── TrustProxies.php │ │ │ └── VerifyCsrfToken.php │ │ ├── Presenter │ │ │ ├── AuthenticationResponseArrayPresenter.php │ │ │ ├── CollectionAsArrayPresenter.php │ │ │ ├── CommentAsArrayPresenter.php │ │ │ ├── LikeArrayPresenter.php │ │ │ ├── TweetArrayPresenter.php │ │ │ └── UserArrayPresenter.php │ │ ├── Request │ │ │ ├── Api │ │ │ │ ├── AddCommentHttpRequest.php │ │ │ │ ├── Auth │ │ │ │ │ ├── LoginHttpRequest.php │ │ │ │ │ ├── RegisterHttpRequest.php │ │ │ │ │ ├── UpdateProfileHttpRequest.php │ │ │ │ │ └── UploadProfileImageHttpRequest.php │ │ │ │ ├── CollectionHttpRequest.php │ │ │ │ └── Tweet │ │ │ │ │ ├── AddTweetHttpRequest.php │ │ │ │ │ ├── UpdateTweetHttpRequest.php │ │ │ │ │ └── UploadTweetImageHttpRequest.php │ │ │ └── ApiFormRequest.php │ │ └── Response │ │ │ └── ApiResponse.php │ ├── Mail │ │ └── WelcomeEmail.php │ ├── Models │ │ ├── Comment.php │ │ ├── Like.php │ │ ├── Tweet.php │ │ └── User.php │ ├── Providers │ │ ├── AppServiceProvider.php │ │ ├── AuthServiceProvider.php │ │ ├── BroadcastServiceProvider.php │ │ ├── EventServiceProvider.php │ │ ├── FakerServiceProvider.php │ │ ├── RouteServiceProvider.php │ │ └── TelescopeServiceProvider.php │ └── Repository │ │ ├── CommentRepository.php │ │ ├── LikeRepository.php │ │ ├── Paginable.php │ │ ├── TweetRepository.php │ │ └── UserRepository.php ├── artisan ├── bootstrap │ ├── app.php │ └── cache │ │ └── .gitignore ├── composer.json ├── composer.lock ├── config │ ├── app.php │ ├── auth.php │ ├── broadcasting.php │ ├── cache.php │ ├── cors.php │ ├── database.php │ ├── filesystems.php │ ├── hashing.php │ ├── jwt.php │ ├── logging.php │ ├── mail.php │ ├── queue.php │ ├── sentry.php │ ├── services.php │ ├── session.php │ ├── telescope.php │ └── view.php ├── database │ ├── .gitignore │ ├── factories │ │ ├── CommentFactory.php │ │ ├── LikeFactory.php │ │ ├── TweetFactory.php │ │ └── UserFactory.php │ ├── migrations │ │ ├── 2014_10_12_000000_create_users_table.php │ │ ├── 2014_10_12_100000_create_password_resets_table.php │ │ ├── 2019_04_08_141946_tweet_table.php │ │ ├── 2019_04_09_093537_comment_table.php │ │ ├── 2019_05_10_104621_add_cascade_delete_to_comment.php │ │ ├── 2019_05_10_140819_add_last_name.php │ │ ├── 2019_05_12_161356_likes_table_migration.php │ │ ├── 2019_05_12_171919_like_is_unique_for_user.php │ │ └── 2019_08_19_000000_create_failed_jobs_table.php │ └── seeders │ │ ├── CommentTableSeeder.php │ │ ├── DatabaseSeeder.php │ │ ├── LikeTableSeeder.php │ │ ├── TweetTableSeeder.php │ │ └── UserTableSeeder.php ├── docker-compose.yml ├── docker │ ├── mysql │ │ └── mysql.cnf │ ├── nginx │ │ └── nginx.conf │ └── php-fpm │ │ ├── Dockerfile │ │ ├── php-ini-overrides.ini │ │ └── xdebug.ini ├── heroku.nginx.conf ├── package.json ├── phpunit.travis.xml ├── phpunit.xml ├── public │ ├── .htaccess │ ├── favicon.ico │ ├── index.php │ ├── robots.txt │ ├── vendor │ │ └── telescope │ │ │ ├── app-dark.css │ │ │ ├── app.css │ │ │ ├── app.js │ │ │ ├── favicon.ico │ │ │ └── mix-manifest.json │ └── web.config ├── resources │ ├── js │ │ ├── app.js │ │ └── bootstrap.js │ ├── lang │ │ └── en │ │ │ ├── auth.php │ │ │ ├── pagination.php │ │ │ ├── passwords.php │ │ │ └── validation.php │ ├── sass │ │ └── app.scss │ └── views │ │ ├── emails │ │ └── welcome.blade.php │ │ └── welcome.blade.php ├── routes │ ├── api.php │ ├── channels.php │ ├── console.php │ └── web.php ├── server.php ├── storage │ ├── app │ │ ├── .gitignore │ │ └── public │ │ │ └── .gitignore │ ├── framework │ │ ├── .gitignore │ │ ├── cache │ │ │ ├── .gitignore │ │ │ └── data │ │ │ │ └── .gitignore │ │ ├── sessions │ │ │ └── .gitignore │ │ ├── testing │ │ │ └── .gitignore │ │ └── views │ │ │ └── .gitignore │ └── logs │ │ └── .gitignore ├── tests │ ├── CreatesApplication.php │ ├── Feature │ │ ├── Api │ │ │ ├── ApiTestCase.php │ │ │ └── V1 │ │ │ │ ├── CommentApiTest.php │ │ │ │ └── TweetApiTest.php │ │ └── ExampleTest.php │ ├── TestCase.php │ └── Unit │ │ └── ExampleTest.php └── webpack.mix.js ├── frontend ├── .browserslistrc ├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── now.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── Api.js │ │ └── ErrorCodes.js │ ├── components │ │ ├── common │ │ │ ├── DefaultAvatar.vue │ │ │ ├── Navbar.vue │ │ │ ├── NoContent.vue │ │ │ ├── TweetPreview.vue │ │ │ └── TweetPreviewList.vue │ │ ├── filter │ │ │ └── filters.js │ │ ├── mixin │ │ │ └── showStatusToast.js │ │ └── view │ │ │ ├── feed │ │ │ ├── FeedContainer.vue │ │ │ └── NewTweetForm.vue │ │ │ ├── profile │ │ │ └── EditProfileForm.vue │ │ │ ├── tweet │ │ │ ├── Comment.vue │ │ │ ├── EditTweetForm.vue │ │ │ ├── NewCommentForm.vue │ │ │ └── TweetContainer.vue │ │ │ └── user │ │ │ └── UserContainer.vue │ ├── main.js │ ├── router.js │ ├── services │ │ ├── EventEmitter.js │ │ ├── Normalizer.js │ │ ├── Pusher.js │ │ └── Storage.js │ ├── store.js │ ├── store │ │ ├── actions.js │ │ ├── getters.js │ │ ├── modules │ │ │ ├── auth │ │ │ │ ├── actions.js │ │ │ │ ├── getters.js │ │ │ │ ├── index.js │ │ │ │ ├── mutationTypes.js │ │ │ │ ├── mutations.js │ │ │ │ └── state.js │ │ │ ├── comment │ │ │ │ ├── actions.js │ │ │ │ ├── getters.js │ │ │ │ ├── index.js │ │ │ │ ├── mutationTypes.js │ │ │ │ ├── mutations.js │ │ │ │ └── state.js │ │ │ └── tweet │ │ │ │ ├── actions.js │ │ │ │ ├── getters.js │ │ │ │ ├── index.js │ │ │ │ ├── mutationTypes.js │ │ │ │ ├── mutations.js │ │ │ │ └── state.js │ │ ├── mutationTypes.js │ │ ├── mutations.js │ │ └── state.js │ ├── styles │ │ └── common.scss │ └── views │ │ ├── Feed.vue │ │ ├── Profile.vue │ │ ├── SignIn.vue │ │ ├── SignUp.vue │ │ ├── Tweet.vue │ │ └── User.vue ├── vue.config.js └── yarn.lock └── readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | 3 | risky: false 4 | 5 | disabled: 6 | - elseif 7 | - single_blank_line_at_eof 8 | - single_import_per_statement 9 | - function_declaration 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | services: 4 | - mysql 5 | 6 | language: php 7 | 8 | php: 9 | - 8.0 10 | 11 | cache: 12 | directories: 13 | - $HOME/.composer/cache 14 | 15 | before_install: 16 | - cd $TRAVIS_BUILD_DIR/backend 17 | - composer validate 18 | - mysql -e 'CREATE DATABASE thread' 19 | 20 | install: 21 | - composer install --no-interaction --prefer-source 22 | 23 | before_script: 24 | - cp .env.travis .env 25 | - php artisan migrate --force 26 | 27 | script: 28 | - vendor/bin/phpunit --coverage-clover=coverage.xml -c phpunit.travis.xml 29 | 30 | after_success: 31 | - bash <(curl -s https://codecov.io/bash) 32 | 33 | 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "directory": "./frontend", 5 | "changeProcessCWD": true 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pavel Nemchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=mysql 12 | DB_HOST=127.0.0.1 13 | DB_PORT=3306 14 | DB_DATABASE=homestead 15 | DB_USERNAME=homestead 16 | DB_PASSWORD=secret 17 | 18 | BROADCAST_DRIVER=log 19 | CACHE_DRIVER=file 20 | FILESYSTEM_DRIVER=public 21 | QUEUE_CONNECTION=sync 22 | SESSION_DRIVER=file 23 | SESSION_LIFETIME=120 24 | 25 | MEMCACHED_HOST=127.0.0.1 26 | 27 | REDIS_HOST=127.0.0.1 28 | REDIS_PASSWORD=null 29 | REDIS_PORT=6379 30 | 31 | MAIL_MAILER=smtp 32 | MAIL_HOST=smtp.mailtrap.io 33 | MAIL_PORT=2525 34 | MAIL_USERNAME=null 35 | MAIL_PASSWORD=null 36 | MAIL_ENCRYPTION=null 37 | MAIL_FROM_ADDRESS= 38 | MAIL_FROM_NAME="${APP_NAME}" 39 | 40 | AWS_ACCESS_KEY_ID= 41 | AWS_SECRET_ACCESS_KEY= 42 | AWS_DEFAULT_REGION=us-east-1 43 | AWS_BUCKET= 44 | AWS_USE_PATH_STYLE_ENDPOINT=false 45 | 46 | PUSHER_APP_ID= 47 | PUSHER_APP_KEY= 48 | PUSHER_APP_SECRET= 49 | PUSHER_APP_CLUSTER=mt1 50 | 51 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 52 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 53 | 54 | JWT_SECRET= 55 | 56 | MYSQL_PORT=33061 57 | APP_PORT=7777 58 | MYSQL_PORT_TEST_DB=33062 59 | 60 | BEANSTALKD_HOST= 61 | -------------------------------------------------------------------------------- /backend/.env.testing: -------------------------------------------------------------------------------- 1 | APP_KEY=base64:70WifS7kyzGyovfVSvZdMIpkQu6lWda873VoTQc0BEg= 2 | 3 | DB_CONNECTION=mysql 4 | DB_HOST=mysql-testing 5 | DB_PORT=3306 6 | DB_DATABASE=thread-testing 7 | DB_USERNAME=user 8 | DB_PASSWORD=secret 9 | 10 | JWT_SECRET=somesecret 11 | -------------------------------------------------------------------------------- /backend/.env.travis: -------------------------------------------------------------------------------- 1 | APP_ENV=testing 2 | APP_KEY=base64:70WifS7kyzGyovfVSvZdMIpkQu6lWda873VoTQc0BEg= 3 | 4 | DB_CONNECTION=mysql 5 | DB_HOST=127.0.0.1 6 | DB_PORT=3306 7 | DB_DATABASE=thread 8 | DB_USERNAME=root 9 | DB_PASSWORD= 10 | 11 | JWT_SECRET=somesecret 12 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.key 5 | /vendor 6 | .env 7 | .phpunit.result.cache 8 | docker-compose.override.yml 9 | Homestead.json 10 | Homestead.yaml 11 | npm-debug.log 12 | yarn-error.log 13 | /.idea 14 | /.vscode 15 | .mysqldata 16 | _ide_helper.php 17 | .phpstorm.meta.php 18 | -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-nginx -C heroku.nginx.conf public/ 2 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/AuthenticationResponse.php: -------------------------------------------------------------------------------- 1 | accessToken; 19 | } 20 | 21 | public function getTokenType(): string 22 | { 23 | return $this->tokenType; 24 | } 25 | 26 | public function getExpiresIn(): int 27 | { 28 | return $this->expiresIn; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/GetAuthenticatedUserAction.php: -------------------------------------------------------------------------------- 1 | user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/LoginAction.php: -------------------------------------------------------------------------------- 1 | $request->getEmail(), 16 | 'password' => $request->getPassword() 17 | ]); 18 | 19 | if (!$token) { 20 | throw new AuthenticationException(); 21 | } 22 | 23 | return new AuthenticationResponse( 24 | $token, 25 | 'bearer', 26 | auth()->factory()->getTTL() * 60 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | email; 18 | } 19 | 20 | public function getPassword(): string 21 | { 22 | return $this->password; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/LogoutAction.php: -------------------------------------------------------------------------------- 1 | userRepository->create([ 20 | 'email' => $request->getEmail(), 21 | 'password' => $request->getPassword(), 22 | 'first_name' => $request->getFirstName(), 23 | 'last_name' => $request->getLastName(), 24 | 'nickname' => $request->getNickname() 25 | ]); 26 | $token = auth()->login($user); 27 | 28 | $this->mailer->to($user)->send(new WelcomeEmail()); 29 | 30 | return new AuthenticationResponse( 31 | $token, 32 | 'bearer', 33 | auth()->factory()->getTTL() * 60 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/RegisterRequest.php: -------------------------------------------------------------------------------- 1 | email; 21 | } 22 | 23 | public function getPassword(): string 24 | { 25 | return $this->password; 26 | } 27 | 28 | public function getFirstName(): string 29 | { 30 | return $this->firstName; 31 | } 32 | 33 | public function getLastName(): string 34 | { 35 | return $this->lastName; 36 | } 37 | 38 | public function getNickname(): string 39 | { 40 | return $this->nickname; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/UpdateProfileAction.php: -------------------------------------------------------------------------------- 1 | email = $request->getEmail() ?: $user->email; 21 | $user->first_name = $request->getFirstName() ?: $user->first_name; 22 | $user->last_name = $request->getLastName() ?: $user->last_name; 23 | $user->nickname = $request->getNickname() ?: $user->nickname; 24 | 25 | $user = $this->userRepository->save($user); 26 | 27 | return new UpdateProfileResponse($user); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/UpdateProfileRequest.php: -------------------------------------------------------------------------------- 1 | email; 20 | } 21 | 22 | public function getFirstName(): ?string 23 | { 24 | return $this->firstName; 25 | } 26 | 27 | public function getLastName(): ?string 28 | { 29 | return $this->lastName; 30 | } 31 | 32 | public function getNickname(): ?string 33 | { 34 | return $this->nickname; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/UpdateProfileResponse.php: -------------------------------------------------------------------------------- 1 | user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/UploadProfileImageAction.php: -------------------------------------------------------------------------------- 1 | getImage(), 25 | $request->getImage()->hashName(), 26 | 'public' 27 | ); 28 | 29 | $user->profile_image = Storage::url($filePath); 30 | 31 | $user = $this->userRepository->save($user); 32 | 33 | return new UploadProfileImageResponse($user); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/UploadProfileImageRequest.php: -------------------------------------------------------------------------------- 1 | image; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Auth/UploadProfileImageResponse.php: -------------------------------------------------------------------------------- 1 | user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Comment/AddCommentAction.php: -------------------------------------------------------------------------------- 1 | tweetRepository->getById($request->getTweetId()); 26 | } catch (ModelNotFoundException) { 27 | throw new TweetNotFoundException(); 28 | } 29 | 30 | $comment = new Comment(); 31 | $comment->author_id = Auth::id(); 32 | $comment->tweet_id = $request->getTweetId(); 33 | $comment->body = $request->getBody(); 34 | 35 | $comment = $this->commentRepository->save($comment); 36 | 37 | return new AddCommentResponse($comment); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/app/Action/Comment/AddCommentRequest.php: -------------------------------------------------------------------------------- 1 | body; 16 | } 17 | 18 | public function getTweetId(): int 19 | { 20 | return $this->tweetId; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/app/Action/Comment/AddCommentResponse.php: -------------------------------------------------------------------------------- 1 | comment; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Comment/GetCommentByIdAction.php: -------------------------------------------------------------------------------- 1 | commentRepository->getById($request->getId()); 19 | 20 | return new GetCommentByIdResponse($comment); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/app/Action/Comment/GetCommentByIdResponse.php: -------------------------------------------------------------------------------- 1 | comment; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Comment/GetCommentCollectionAction.php: -------------------------------------------------------------------------------- 1 | commentRepository->paginate( 21 | $request->getPage() ?: CommentRepository::DEFAULT_PAGE, 22 | CommentRepository::DEFAULT_PER_PAGE, 23 | $request->getSort() ?: CommentRepository::DEFAULT_SORT, 24 | $request->getDirection() ?: CommentRepository::DEFAULT_DIRECTION 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/app/Action/Comment/GetCommentCollectionByTweetIdAction.php: -------------------------------------------------------------------------------- 1 | commentRepository->getPaginatedByTweetId( 20 | $request->getTweetId(), 21 | $request->getPage() ?: CommentRepository::DEFAULT_PAGE, 22 | CommentRepository::DEFAULT_PER_PAGE, 23 | $request->getSort() ?: CommentRepository::DEFAULT_SORT, 24 | $request->getDirection() ?: CommentRepository::DEFAULT_DIRECTION 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/app/Action/Comment/GetCommentCollectionByTweetIdRequest.php: -------------------------------------------------------------------------------- 1 | tweetId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/app/Action/GetByIdRequest.php: -------------------------------------------------------------------------------- 1 | id; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Action/GetCollectionRequest.php: -------------------------------------------------------------------------------- 1 | page; 19 | } 20 | 21 | public function getSort(): ?string 22 | { 23 | return $this->sort; 24 | } 25 | 26 | public function getDirection(): ?string 27 | { 28 | return $this->direction; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/app/Action/PaginatedResponse.php: -------------------------------------------------------------------------------- 1 | paginator; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/AddTweetAction.php: -------------------------------------------------------------------------------- 1 | author_id = Auth::id(); 22 | $tweet->text = $request->getText(); 23 | 24 | $tweet = $this->tweetRepository->save($tweet); 25 | 26 | broadcast(new TweetAddedEvent($tweet))->toOthers(); 27 | 28 | return new AddTweetResponse($tweet); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/AddTweetRequest.php: -------------------------------------------------------------------------------- 1 | text; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/AddTweetResponse.php: -------------------------------------------------------------------------------- 1 | tweet; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/DeleteTweetAction.php: -------------------------------------------------------------------------------- 1 | tweetRepository->getById($request->getId()); 23 | } catch (ModelNotFoundException) { 24 | throw new TweetNotFoundException(); 25 | } 26 | 27 | if ($tweet->author_id !== Auth::id()) { 28 | throw new AuthorizationException(); 29 | } 30 | 31 | $this->tweetRepository->delete($tweet); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/DeleteTweetRequest.php: -------------------------------------------------------------------------------- 1 | id; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/GetTweetByIdAction.php: -------------------------------------------------------------------------------- 1 | tweetRepository->getById($request->getId()); 19 | 20 | return new GetTweetByIdResponse($tweet); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/GetTweetByIdResponse.php: -------------------------------------------------------------------------------- 1 | tweet; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/GetTweetCollectionAction.php: -------------------------------------------------------------------------------- 1 | tweetRepository->paginate( 21 | $request->getPage() ?: TweetRepository::DEFAULT_PAGE, 22 | TweetRepository::DEFAULT_PER_PAGE, 23 | $request->getSort() ?: TweetRepository::DEFAULT_SORT, 24 | $request->getDirection() ?: TweetRepository::DEFAULT_DIRECTION 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/GetTweetCollectionByUserIdAction.php: -------------------------------------------------------------------------------- 1 | tweetRepository->getPaginatedByUserId( 20 | $request->getUserId(), 21 | $request->getPage() ?: TweetRepository::DEFAULT_PAGE, 22 | TweetRepository::DEFAULT_PER_PAGE, 23 | $request->getSort() ?: TweetRepository::DEFAULT_SORT, 24 | $request->getDirection() ?: TweetRepository::DEFAULT_DIRECTION 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/GetTweetCollectionByUserIdRequest.php: -------------------------------------------------------------------------------- 1 | userId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/LikeTweetAction.php: -------------------------------------------------------------------------------- 1 | tweetRepository->getById($request->getTweetId()); 26 | 27 | $userId = Auth::id(); 28 | 29 | // if user already liked tweet, we remove previous like 30 | if ($this->likeRepository->existsForTweetByUser($tweet->id, $userId)) { 31 | $this->likeRepository->deleteForTweetByUser($tweet->id, $userId); 32 | 33 | return new LikeTweetResponse(self::REMOVE_LIKE_STATUS); 34 | } 35 | 36 | $like = new Like(); 37 | $like->forTweet(Auth::id(), $tweet->id); 38 | 39 | $this->likeRepository->save($like); 40 | 41 | return new LikeTweetResponse(self::ADD_LIKE_STATUS); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/LikeTweetRequest.php: -------------------------------------------------------------------------------- 1 | tweetId; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/LikeTweetResponse.php: -------------------------------------------------------------------------------- 1 | status; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/UpdateTweetAction.php: -------------------------------------------------------------------------------- 1 | tweetRepository->getById($request->getId()); 23 | } catch (ModelNotFoundException) { 24 | throw new TweetNotFoundException(); 25 | } 26 | 27 | if ($tweet->author_id !== Auth::id()) { 28 | throw new AuthorizationException(); 29 | } 30 | 31 | $tweet->text = $request->getText() ?: $tweet->text; 32 | 33 | $tweet = $this->tweetRepository->save($tweet); 34 | 35 | return new UpdateTweetResponse($tweet); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/UpdateTweetRequest.php: -------------------------------------------------------------------------------- 1 | id; 16 | } 17 | 18 | public function getText(): ?string 19 | { 20 | return $this->text; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/UpdateTweetResponse.php: -------------------------------------------------------------------------------- 1 | tweet; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/UploadTweetImageAction.php: -------------------------------------------------------------------------------- 1 | tweetRepository->getById($request->getId()); 25 | } catch (ModelNotFoundException) { 26 | throw new TweetNotFoundException(); 27 | } 28 | 29 | if ($tweet->author_id !== Auth::id()) { 30 | throw new AuthorizationException(); 31 | } 32 | 33 | $filePath = Storage::putFileAs( 34 | Config::get('filesystems.tweet_images_dir'), 35 | $request->getImage(), 36 | $request->getImage()->hashName(), 37 | 'public' 38 | ); 39 | 40 | $tweet->image_url = Storage::url($filePath); 41 | 42 | $tweet = $this->tweetRepository->save($tweet); 43 | 44 | return new UploadTweetImageResponse($tweet); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/UploadTweetImageRequest.php: -------------------------------------------------------------------------------- 1 | id; 18 | } 19 | 20 | public function getImage(): UploadedFile 21 | { 22 | return $this->image; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/app/Action/Tweet/UploadTweetImageResponse.php: -------------------------------------------------------------------------------- 1 | tweet; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/User/GetUserByIdAction.php: -------------------------------------------------------------------------------- 1 | userRepository->getById($request->getId()); 19 | 20 | return new GetUserByIdResponse($user); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/app/Action/User/GetUserByIdResponse.php: -------------------------------------------------------------------------------- 1 | user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Action/User/GetUserCollectionAction.php: -------------------------------------------------------------------------------- 1 | userRepository->paginate( 21 | $request->getPage() ?: UserRepository::DEFAULT_PAGE, 22 | UserRepository::DEFAULT_PER_PAGE, 23 | $request->getSort() ?: UserRepository::DEFAULT_SORT, 24 | $request->getDirection() ?: UserRepository::DEFAULT_DIRECTION 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 19 | } 20 | 21 | /** 22 | * Register the commands for the application. 23 | * 24 | * @return void 25 | */ 26 | protected function commands() 27 | { 28 | $this->load(__DIR__ . '/Commands'); 29 | 30 | require base_path('routes/console.php'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/app/Events/TweetAddedEvent.php: -------------------------------------------------------------------------------- 1 | tweet = App::make(TweetArrayPresenter::class)->present($tweet); 25 | } 26 | 27 | public function broadcastAs(): string 28 | { 29 | return 'tweet.added'; 30 | } 31 | 32 | public function broadcastOn(): PrivateChannel 33 | { 34 | return new PrivateChannel('tweets'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/app/Exceptions/ErrorCode.php: -------------------------------------------------------------------------------- 1 | execute(new LikeTweetRequest((int)$id)); 19 | 20 | return $this->createSuccessResponse(['status' => $response->getStatus()]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/Api/UserController.php: -------------------------------------------------------------------------------- 1 | execute( 24 | new GetCollectionRequest( 25 | (int)$request->query('page'), 26 | $request->query('sort'), 27 | $request->query('direction') 28 | ) 29 | ); 30 | 31 | return $this->createPaginatedResponse($response->getPaginator(), $presenter); 32 | } 33 | 34 | public function getUserById( 35 | GetUserByIdAction $action, 36 | UserArrayPresenter $presenter, 37 | string $id, 38 | ): ApiResponse { 39 | $user = $action->execute(new GetByIdRequest((int)$id))->getUser(); 40 | 41 | return $this->createSuccessResponse($presenter->present($user)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/ApiController.php: -------------------------------------------------------------------------------- 1 | presentCollection(Collection::make($paginator->items())), 42 | $paginator->total(), 43 | $paginator->perPage(), 44 | $paginator->currentPage() 45 | ) 46 | ); 47 | } 48 | 49 | final protected function created(array $data): ApiResponse 50 | { 51 | return ApiResponse::created($data); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/Auth/ConfirmPasswordController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/Auth/ForgotPasswordController.php: -------------------------------------------------------------------------------- 1 | middleware('guest')->except('logout'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/Auth/RegisterController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 39 | } 40 | 41 | /** 42 | * Get a validator for an incoming registration request. 43 | * 44 | * @param array $data 45 | * @return \Illuminate\Contracts\Validation\Validator 46 | */ 47 | protected function validator(array $data) 48 | { 49 | return Validator::make($data, [ 50 | 'name' => ['required', 'string', 'max:255'], 51 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 52 | 'password' => ['required', 'string', 'min:8', 'confirmed'], 53 | ]); 54 | } 55 | 56 | /** 57 | * Create a new user instance after a valid registration. 58 | * 59 | * @param array $data 60 | * @return User 61 | */ 62 | protected function create(array $data) 63 | { 64 | return User::create([ 65 | 'name' => $data['name'], 66 | 'email' => $data['email'], 67 | 'password' => Hash::make($data['password']), 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/Auth/ResetPasswordController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 38 | $this->middleware('signed')->only('verify'); 39 | $this->middleware('throttle:6,1')->only('verify', 'resend'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $middleware = [ 17 | // \App\Http\Middleware\TrustHosts::class, 18 | \App\Http\Middleware\TrustProxies::class, 19 | \Fruitcake\Cors\HandleCors::class, 20 | \App\Http\Middleware\PreventRequestsDuringMaintenance::class, 21 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 22 | \App\Http\Middleware\TrimStrings::class, 23 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 24 | ]; 25 | 26 | /** 27 | * The application's route middleware groups. 28 | * 29 | * @var array> 30 | */ 31 | protected $middlewareGroups = [ 32 | 'web' => [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 37 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 38 | \App\Http\Middleware\VerifyCsrfToken::class, 39 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 40 | ], 41 | 42 | 'api' => [ 43 | // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 44 | 'throttle:api', 45 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 46 | ], 47 | ]; 48 | 49 | /** 50 | * The application's route middleware. 51 | * 52 | * These middleware may be assigned to groups or used individually. 53 | * 54 | * @var array 55 | */ 56 | protected $routeMiddleware = [ 57 | 'auth' => \App\Http\Middleware\Authenticate::class, 58 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 59 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 60 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 61 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 62 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 63 | 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 64 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 65 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | // return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 26 | return redirect(RouteServiceProvider::HOME); 27 | } 28 | } 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'api/*' 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Http/Presenter/AuthenticationResponseArrayPresenter.php: -------------------------------------------------------------------------------- 1 | $response->getAccessToken(), 15 | 'token_type' => $response->getTokenType(), 16 | 'expires_in' => $response->getExpiresIn() 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Http/Presenter/CollectionAsArrayPresenter.php: -------------------------------------------------------------------------------- 1 | userArrayPresenter = $userArrayPresenter; 17 | } 18 | 19 | public function present(Comment $comment): array 20 | { 21 | return [ 22 | 'id' => $comment->getId(), 23 | 'body' => $comment->getBody(), 24 | 'author_id' => $comment->getAuthorId(), 25 | 'tweet_id' => $comment->getTweetId(), 26 | 'created_at' => $comment->getCreatedAt()->toDateTimeString(), 27 | 'updated_at' => $comment->getUpdatedAt() ? $comment->getUpdatedAt()->toDateTimeString() : null, 28 | 'author' => $this->userArrayPresenter->present($comment->author) 29 | ]; 30 | } 31 | 32 | public function presentCollection(Collection $collection): array 33 | { 34 | return $collection 35 | ->map( 36 | function (Comment $comment) { 37 | return $this->present($comment); 38 | } 39 | ) 40 | ->all(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/app/Http/Presenter/LikeArrayPresenter.php: -------------------------------------------------------------------------------- 1 | $like->getUserId(), 16 | 'created_at' => $like->getCreatedAt()->toDateTimeString() 17 | ]; 18 | } 19 | 20 | public function presentCollection(Collection $collection): array 21 | { 22 | return $collection 23 | ->map( 24 | function (Like $like) { 25 | return $this->present($like); 26 | } 27 | ) 28 | ->all(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/app/Http/Presenter/TweetArrayPresenter.php: -------------------------------------------------------------------------------- 1 | userPresenter = $userPresenter; 18 | $this->likeArrayPresenter = $likeArrayPresenter; 19 | } 20 | 21 | public function present(Tweet $tweet): array 22 | { 23 | return [ 24 | 'id' => $tweet->getId(), 25 | 'text' => $tweet->getText(), 26 | 'image_url' => $tweet->getImageUrl(), 27 | 'created_at' => $tweet->getCreatedAt()->toDateTimeString(), 28 | 'author' => $this->userPresenter->present($tweet->getAuthor()), 29 | 'comments_count' => $tweet->getCommentsCount(), 30 | 'likes_count' => $tweet->getLikesCount(), 31 | 'likes' => $this->likeArrayPresenter->presentCollection($tweet->likes) 32 | ]; 33 | } 34 | 35 | public function presentCollection(Collection $collection): array 36 | { 37 | return $collection 38 | ->map( 39 | function (Tweet $tweet) { 40 | return $this->present($tweet); 41 | } 42 | ) 43 | ->all(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/app/Http/Presenter/UserArrayPresenter.php: -------------------------------------------------------------------------------- 1 | $user->getId(), 16 | 'email' => $user->getEmail(), 17 | 'first_name' => $user->getFirstName(), 18 | 'last_name' => $user->getLastName(), 19 | 'nickname' => $user->getNickName(), 20 | 'avatar' => $user->getAvatar() 21 | ]; 22 | } 23 | 24 | public function presentCollection(Collection $collection): array 25 | { 26 | return $collection 27 | ->map( 28 | function (User $user) { 29 | return $this->present($user); 30 | } 31 | ) 32 | ->all(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/AddCommentHttpRequest.php: -------------------------------------------------------------------------------- 1 | 'required', 15 | 'tweet_id' => 'required|integer|min:1', 16 | ]; 17 | } 18 | } -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/Auth/LoginHttpRequest.php: -------------------------------------------------------------------------------- 1 | 'required|email', 20 | 'password' => 'required|min:6|string', 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/Auth/RegisterHttpRequest.php: -------------------------------------------------------------------------------- 1 | 'required|email|unique:users', 20 | 'password' => 'required|min:6|string', 21 | 'first_name' => 'required|string|min:2', 22 | 'last_name' => 'required|string|min:2', 23 | 'nickname' => 'required|string|min:2|unique:users' 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/Auth/UpdateProfileHttpRequest.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'email', 23 | Rule::unique('users')->ignore(Auth::id()) 24 | ], 25 | 'first_name' => 'string|min:2', 26 | 'last_name' => 'string|min:2', 27 | 'nickname' => [ 28 | 'string', 29 | 'min:2', 30 | Rule::unique('users')->ignore(Auth::id()) 31 | ] 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/Auth/UploadProfileImageHttpRequest.php: -------------------------------------------------------------------------------- 1 | 'required|image' 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/CollectionHttpRequest.php: -------------------------------------------------------------------------------- 1 | 'integer|min:1', 16 | 'sort' => 'string', 17 | 'direction' => [ 18 | 'string', 19 | Rule::in(['asc', 'desc']) 20 | ] 21 | ]; 22 | } 23 | } -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/Tweet/AddTweetHttpRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string' 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/Tweet/UpdateTweetHttpRequest.php: -------------------------------------------------------------------------------- 1 | 'string' 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Http/Request/Api/Tweet/UploadTweetImageHttpRequest.php: -------------------------------------------------------------------------------- 1 | 'required|image' 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/app/Http/Request/ApiFormRequest.php: -------------------------------------------------------------------------------- 1 | subject('Welcome!')->view('emails.welcome'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/app/Models/Comment.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class, 'author_id'); 41 | } 42 | 43 | public function tweet(): BelongsTo 44 | { 45 | return $this->belongsTo(Tweet::class); 46 | } 47 | 48 | public function getId(): int 49 | { 50 | return $this->id; 51 | } 52 | 53 | public function getBody(): string 54 | { 55 | return $this->body; 56 | } 57 | 58 | public function getCreatedAt(): Carbon 59 | { 60 | return $this->created_at; 61 | } 62 | 63 | public function getUpdatedAt(): ?Carbon 64 | { 65 | return $this->updated_at; 66 | } 67 | 68 | public function getAuthorId(): int 69 | { 70 | return $this->author_id; 71 | } 72 | 73 | public function edit(string $text): void 74 | { 75 | if (empty($text)) { 76 | throw new InvalidArgumentException('Comment body cannot be empty.'); 77 | } 78 | 79 | $this->body = $text; 80 | } 81 | 82 | public function getTweetId(): int 83 | { 84 | return $this->tweet_id; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /backend/app/Models/Like.php: -------------------------------------------------------------------------------- 1 | morphTo(); 46 | } 47 | 48 | public function user(): BelongsTo 49 | { 50 | return $this->belongsTo(User::class); 51 | } 52 | 53 | public function getId(): int 54 | { 55 | return $this->id; 56 | } 57 | 58 | public function getCreatedAt(): Carbon 59 | { 60 | return Carbon::createFromTimeString($this->created_at); 61 | } 62 | 63 | public function getUserId(): int 64 | { 65 | return $this->user_id; 66 | } 67 | 68 | public function getUser(): User 69 | { 70 | return $this->user; 71 | } 72 | 73 | public function forTweet(int $userId, int $tweetId): void 74 | { 75 | $this->assertIdIsValid($userId); 76 | $this->assertIdIsValid($tweetId); 77 | 78 | $this->user_id = $userId; 79 | $this->likeable_id = $tweetId; 80 | $this->likeable_type = Tweet::class; 81 | $this->created_at = Carbon::now(); 82 | } 83 | 84 | /** 85 | * @param int $id 86 | * @throws InvalidArgumentException 87 | */ 88 | private function assertIdIsValid(int $id): void 89 | { 90 | if ($id < 1) { 91 | throw new InvalidArgumentException('Id cannot be less than 1.'); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /backend/app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->isLocal()) { 18 | $this->app->register(TelescopeServiceProvider::class); 19 | $this->app->register(IdeHelperServiceProvider::class); 20 | } 21 | 22 | $this->app->register(FakerServiceProvider::class); 23 | } 24 | 25 | /** 26 | * Bootstrap any application services. 27 | * 28 | * @return void 29 | */ 30 | public function boot() 31 | { 32 | // 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | 28 | // 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | ['api'] 19 | ]); 20 | 21 | require base_path('routes/channels.php'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | protected $listen = [ 17 | Registered::class => [ 18 | SendEmailVerificationNotification::class, 19 | ], 20 | ]; 21 | 22 | /** 23 | * Register any events for your application. 24 | * 25 | * @return void 26 | */ 27 | public function boot() 28 | { 29 | // 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/app/Providers/FakerServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(Generator::class, function () { 20 | $faker = Factory::create(); 21 | $faker->addProvider(new PicsumProvider($faker)); 22 | 23 | return $faker; 24 | }); 25 | } 26 | 27 | /** 28 | * Bootstrap services. 29 | * 30 | * @return void 31 | */ 32 | public function boot() 33 | { 34 | // 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 39 | 40 | $this->routes(function () { 41 | Route::prefix('api') 42 | ->middleware('api') 43 | ->namespace($this->namespace) 44 | ->group(base_path('routes/api.php')); 45 | 46 | Route::middleware('web') 47 | ->namespace($this->namespace) 48 | ->group(base_path('routes/web.php')); 49 | }); 50 | } 51 | 52 | /** 53 | * Configure the rate limiters for the application. 54 | * 55 | * @return void 56 | */ 57 | protected function configureRateLimiting() 58 | { 59 | RateLimiter::for('api', function (Request $request) { 60 | return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/app/Providers/TelescopeServiceProvider.php: -------------------------------------------------------------------------------- 1 | hideSensitiveRequestDetails(); 22 | 23 | Telescope::filter(function (IncomingEntry $entry) { 24 | if ($this->app->isLocal()) { 25 | return true; 26 | } 27 | 28 | return $entry->isReportableException() || 29 | $entry->isFailedJob() || 30 | $entry->isScheduledTask() || 31 | $entry->hasMonitoredTag(); 32 | }); 33 | } 34 | 35 | /** 36 | * Prevent sensitive request details from being logged by Telescope. 37 | * 38 | * @return void 39 | */ 40 | protected function hideSensitiveRequestDetails() 41 | { 42 | if ($this->app->isLocal()) { 43 | return; 44 | } 45 | 46 | Telescope::hideRequestParameters(['_token']); 47 | 48 | Telescope::hideRequestHeaders([ 49 | 'cookie', 50 | 'x-csrf-token', 51 | 'x-xsrf-token', 52 | ]); 53 | } 54 | 55 | /** 56 | * Register the Telescope gate. 57 | * 58 | * This gate determines who can access Telescope in non-local environments. 59 | * 60 | * @return void 61 | */ 62 | protected function gate() 63 | { 64 | Gate::define('viewTelescope', function ($user) { 65 | return in_array($user->email, [ 66 | // 67 | ]); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /backend/app/Repository/CommentRepository.php: -------------------------------------------------------------------------------- 1 | paginate($perPage, ['*'], null, $page); 30 | } 31 | 32 | public function getPaginatedByTweetId( 33 | int $tweetId, 34 | int $page = self::DEFAULT_PAGE, 35 | int $perPage = self::DEFAULT_PER_PAGE, 36 | string $sort = self::DEFAULT_SORT, 37 | string $direction = self::DEFAULT_DIRECTION 38 | ): LengthAwarePaginator { 39 | return Comment::where('tweet_id', $tweetId) 40 | ->orderBy($sort, $direction) 41 | ->paginate($perPage, ['*'], null, $page); 42 | } 43 | 44 | public function save(Comment $comment): Comment 45 | { 46 | $comment->save(); 47 | 48 | return $comment; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /backend/app/Repository/LikeRepository.php: -------------------------------------------------------------------------------- 1 | save(); 15 | 16 | return $like; 17 | } 18 | 19 | public function existsForTweetByUser(int $tweetId, int $userId): bool 20 | { 21 | return Like::where([ 22 | 'likeable_id' => $tweetId, 23 | 'likeable_type' => Tweet::class, 24 | 'user_id' => $userId 25 | ])->exists(); 26 | } 27 | 28 | public function deleteForTweetByUser(int $tweetId, int $userId): void 29 | { 30 | Like::where([ 31 | 'likeable_id' => $tweetId, 32 | 'likeable_type' => Tweet::class, 33 | 'user_id' => $userId 34 | ])->delete(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/app/Repository/Paginable.php: -------------------------------------------------------------------------------- 1 | paginate($perPage, ['*'], null, $page); 25 | } 26 | 27 | /** 28 | * @param int $id 29 | * @return Tweet 30 | * @throws ModelNotFoundException 31 | */ 32 | public function getById(int $id): Tweet 33 | { 34 | return Tweet::findOrFail($id); 35 | } 36 | 37 | public function getPaginatedByUserId( 38 | int $userId, 39 | int $page = self::DEFAULT_PAGE, 40 | int $perPage = self::DEFAULT_PER_PAGE, 41 | string $sort = self::DEFAULT_SORT, 42 | string $direction = self::DEFAULT_DIRECTION 43 | ): LengthAwarePaginator { 44 | return Tweet::where('author_id', $userId) 45 | ->orderBy($sort, $direction) 46 | ->paginate($perPage, ['*'], null, $page); 47 | } 48 | 49 | public function save(Tweet $tweet): Tweet 50 | { 51 | $tweet->save(); 52 | 53 | return $tweet; 54 | } 55 | 56 | public function delete(Tweet $tweet): ?bool 57 | { 58 | return $tweet->delete(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/app/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | paginate($perPage, ['*'], null, $page); 25 | } 26 | 27 | /** 28 | * @param int $id 29 | * @return User 30 | * @throws ModelNotFoundException 31 | */ 32 | public function getById(int $id): User 33 | { 34 | return User::findOrFail($id); 35 | } 36 | 37 | public function save(User $user): User 38 | { 39 | $user->save(); 40 | 41 | return $user; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /backend/bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /backend/bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": ["framework", "laravel"], 6 | "license": "MIT", 7 | "require": { 8 | "php": "^8.0", 9 | "ext-bcmath": "*", 10 | "ext-mbstring": "*", 11 | "doctrine/dbal": "^2.9", 12 | "fideloper/proxy": "^4.4", 13 | "fruitcake/laravel-cors": "^2.0", 14 | "guzzlehttp/guzzle": "^7.0.1", 15 | "laravel/framework": "^8.65", 16 | "laravel/tinker": "^2.5", 17 | "league/flysystem-aws-s3-v3": "^1.0", 18 | "pda/pheanstalk": "^4.0", 19 | "predis/predis": "^1.1", 20 | "pusher/pusher-php-server": "^7.0", 21 | "sentry/sentry-laravel": "^2.10", 22 | "tymon/jwt-auth": "1.0.*" 23 | }, 24 | "require-dev": { 25 | "barryvdh/laravel-ide-helper": "^2.6", 26 | "facade/ignition": "^2.5", 27 | "fakerphp/faker": "^1.16.0", 28 | "laravel/telescope": "^4.0", 29 | "mmo/faker-images": "^0.6.0", 30 | "mockery/mockery": "^1.4.4", 31 | "nunomaduro/collision": "^5.10", 32 | "phpunit/phpunit": "^9.5.10" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "App\\": "app/", 37 | "Database\\Factories\\": "database/factories/", 38 | "Database\\Seeders\\": "database/seeders/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Tests\\": "tests/" 44 | } 45 | }, 46 | "scripts": { 47 | "post-autoload-dump": [ 48 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 49 | "@php artisan package:discover --ansi" 50 | ], 51 | "post-update-cmd": [ 52 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force", 53 | "@php artisan ide-helper:generate", 54 | "@php artisan ide-helper:meta" 55 | ], 56 | "post-root-package-install": [ 57 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 58 | ], 59 | "post-create-project-cmd": [ 60 | "@php artisan key:generate --ansi" 61 | ] 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "dont-discover": [] 66 | } 67 | }, 68 | "config": { 69 | "optimize-autoloader": true, 70 | "preferred-install": "dist", 71 | "sort-packages": true 72 | }, 73 | "minimum-stability": "dev", 74 | "prefer-stable": true 75 | } 76 | -------------------------------------------------------------------------------- /backend/config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'encrypted' => true, 41 | ], 42 | ], 43 | 44 | 'ably' => [ 45 | 'driver' => 'ably', 46 | 'key' => env('ABLY_KEY'), 47 | ], 48 | 49 | 'redis' => [ 50 | 'driver' => 'redis', 51 | 'connection' => 'default', 52 | ], 53 | 54 | 'log' => [ 55 | 'driver' => 'log', 56 | ], 57 | 58 | 'null' => [ 59 | 'driver' => 'null', 60 | ], 61 | 62 | ], 63 | 64 | ]; 65 | -------------------------------------------------------------------------------- /backend/config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /backend/config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DRIVER', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been setup for each driver as an example of the required options. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | ], 37 | 38 | 'public' => [ 39 | 'driver' => 'local', 40 | 'root' => storage_path('app/public'), 41 | 'url' => env('APP_URL') . '/storage', 42 | 'visibility' => 'public', 43 | ], 44 | 45 | 's3' => [ 46 | 'driver' => 's3', 47 | 'key' => env('AWS_ACCESS_KEY_ID'), 48 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 49 | 'region' => env('AWS_DEFAULT_REGION'), 50 | 'bucket' => env('AWS_BUCKET'), 51 | 'url' => env('AWS_URL'), 52 | 'endpoint' => env('AWS_ENDPOINT'), 53 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 54 | ], 55 | 56 | ], 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Symbolic Links 61 | |-------------------------------------------------------------------------- 62 | | 63 | | Here you may configure the symbolic links that will be created when the 64 | | `storage:link` Artisan command is executed. The array keys should be 65 | | the locations of the links and the values should be their targets. 66 | | 67 | */ 68 | 69 | 'links' => [ 70 | public_path('storage') => storage_path('app/public'), 71 | ], 72 | 73 | 'tweet_images_dir' => 'tweet-images', 74 | 'profile_images_dir' => 'profile-images' 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /backend/config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 1024, 48 | 'threads' => 2, 49 | 'time' => 2, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /backend/config/sentry.php: -------------------------------------------------------------------------------- 1 | env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')), 6 | 7 | // capture release as git sha 8 | // 'release' => trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')), 9 | 10 | 'breadcrumbs' => [ 11 | 12 | // Capture bindings on SQL queries logged in breadcrumbs 13 | 'sql_bindings' => true, 14 | 15 | ], 16 | 17 | ]; 18 | -------------------------------------------------------------------------------- /backend/config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 31 | ], 32 | 33 | 'stripe' => [ 34 | 'model' => App\User::class, 35 | 'key' => env('STRIPE_KEY'), 36 | 'secret' => env('STRIPE_SECRET'), 37 | 'webhook' => [ 38 | 'secret' => env('STRIPE_WEBHOOK_SECRET'), 39 | 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), 40 | ], 41 | ], 42 | 43 | ]; 44 | -------------------------------------------------------------------------------- /backend/config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /backend/database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /backend/database/factories/CommentFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->text(), 20 | 'author_id' => User::factory(), 21 | 'tweet_id' => Tweet::factory(), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/database/factories/LikeFactory.php: -------------------------------------------------------------------------------- 1 | faker->randomElement([ 21 | Tweet::class, 22 | Comment::class 23 | ]); 24 | 25 | return [ 26 | 'user_id' => User::factory(), 27 | 'likeable_id' => $likeable::factory(), 28 | 'likeable_type' => $likeable, 29 | 'created_at' => Carbon::now(), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/database/factories/TweetFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->text(), 19 | 'author_id' => User::factory(), 20 | 'image_url' => $this->faker->randomElement([true, false]) 21 | ? $this->faker->unique()->picsumUrl() 22 | : null, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->firstName, 19 | 'last_name' => $this->faker->lastName, 20 | 'email' => $this->faker->unique()->safeEmail, 21 | 'email_verified_at' => now(), 22 | 'password' => 'password', 23 | 'remember_token' => Str::random(10), 24 | 'nickname' => $this->faker->unique()->userName, 25 | 'profile_image' => $this->faker->unique()->picsumUrl() 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->string('nickname')->unique(); 23 | $table->string('profile_image')->nullable(); 24 | $table->rememberToken(); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('users'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_04_08_141946_tweet_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $blueprint->longText('text'); 19 | $blueprint->unsignedBigInteger('author_id'); 20 | $blueprint->string('image_url')->nullable(); 21 | $blueprint->timestamps(); 22 | 23 | $blueprint 24 | ->foreign('author_id') 25 | ->references('id') 26 | ->on('users'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('tweets'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_04_09_093537_comment_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $blueprint->longText('body'); 19 | $blueprint->unsignedBigInteger('author_id'); 20 | $blueprint->unsignedBigInteger('tweet_id'); 21 | $blueprint->timestamps(); 22 | 23 | $blueprint 24 | ->foreign('author_id') 25 | ->references('id') 26 | ->on('users'); 27 | 28 | $blueprint 29 | ->foreign('tweet_id') 30 | ->references('id') 31 | ->on('tweets'); 32 | }); 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | * 38 | * @return void 39 | */ 40 | public function down() 41 | { 42 | Schema::dropIfExists('comments'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_05_10_104621_add_cascade_delete_to_comment.php: -------------------------------------------------------------------------------- 1 | dropForeign(['tweet_id']); 18 | $table->foreign('tweet_id') 19 | ->references('id') 20 | ->on('tweets') 21 | ->onDelete('cascade'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::table('comments', function (Blueprint $table) { 33 | $table->dropForeign(['tweet_id']); 34 | $table->foreign('tweet_id') 35 | ->references('id') 36 | ->on('tweets'); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_05_10_140819_add_last_name.php: -------------------------------------------------------------------------------- 1 | renameColumn('name', 'first_name'); 18 | $table->string('last_name'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('users', function (Blueprint $table) { 30 | $table->dropColumn('last_name'); 31 | $table->renameColumn('first_name', 'name'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_05_12_161356_likes_table_migration.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->unsignedBigInteger('user_id'); 19 | $table->unsignedBigInteger('likeable_id'); 20 | $table->string('likeable_type'); 21 | $table->dateTime('created_at'); 22 | 23 | $table 24 | ->foreign('user_id') 25 | ->references('id') 26 | ->on('users') 27 | ->onDelete('cascade'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('likes'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_05_12_171919_like_is_unique_for_user.php: -------------------------------------------------------------------------------- 1 | unique(['user_id', 'likeable_id', 'likeable_type']); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('likes', function(Blueprint $blueprint) { 30 | // need to drop foreign key before we can drop unique index 31 | $blueprint->dropForeign('likes_user_id_foreign'); 32 | $blueprint->dropUnique(['user_id', 'likeable_id', 'likeable_type']); 33 | $blueprint 34 | ->foreign('user_id') 35 | ->references('id') 36 | ->on('users') 37 | ->onDelete('cascade'); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('uuid')->unique(); 19 | $table->text('connection'); 20 | $table->text('queue'); 21 | $table->longText('payload'); 22 | $table->longText('exception'); 23 | $table->timestamp('failed_at')->useCurrent(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('failed_jobs'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/database/seeders/CommentTableSeeder.php: -------------------------------------------------------------------------------- 1 | random(3) as $user) { 24 | Comment::factory() 25 | ->for($tweet) 26 | ->for($user, 'author') 27 | ->create(); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UserTableSeeder::class); 17 | $this->call(TweetTableSeeder::class); 18 | $this->call(CommentTableSeeder::class); 19 | $this->call(LikeTableSeeder::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/database/seeders/LikeTableSeeder.php: -------------------------------------------------------------------------------- 1 | random(20) as $tweet) { 25 | foreach ($users->random(10) as $user) { 26 | Like::factory() 27 | ->for($user) 28 | ->for($tweet, 'likeable') 29 | ->create(); 30 | } 31 | } 32 | 33 | foreach ($comments->random(20) as $comment) { 34 | foreach ($users->random(10) as $user) { 35 | Like::factory() 36 | ->for($user) 37 | ->for($comment, 'likeable') 38 | ->create(); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/database/seeders/TweetTableSeeder.php: -------------------------------------------------------------------------------- 1 | for($user, 'author')->count(2)->create(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/database/seeders/UserTableSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | webserver: 5 | image: nginx:stable-alpine 6 | container_name: thread-webserver 7 | working_dir: /var/www/app 8 | ports: 9 | - "${APP_PORT}:80" 10 | volumes: 11 | - .:/var/www/app 12 | - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf 13 | 14 | app: 15 | build: ./docker/php-fpm 16 | container_name: thread-app 17 | working_dir: /var/www/app 18 | volumes: 19 | - .:/var/www/app 20 | - ./docker/php-fpm/php-ini-overrides.ini:/usr/local/etc/php/conf.d/99-overrides.ini 21 | 22 | mysql: 23 | image: mysql:5.7.22 24 | container_name: thread-mysql 25 | working_dir: /var/www/app 26 | volumes: 27 | - .mysqldata:/var/lib/mysql 28 | - ./docker/mysql:/etc/mysql/conf.d 29 | ports: 30 | - "${MYSQL_PORT}:3306" 31 | environment: 32 | - MYSQL_ROOT_PASSWORD=root 33 | - MYSQL_DATABASE=thread 34 | - MYSQL_USER=user 35 | - MYSQL_PASSWORD=secret 36 | 37 | mysql-testing: 38 | image: mysql:5.7.22 39 | container_name: thread-mysql-testing 40 | volumes: 41 | - ./docker/mysql:/etc/mysql/conf.d 42 | ports: 43 | - "${MYSQL_PORT_TEST_DB}:3306" 44 | environment: 45 | - MYSQL_ROOT_PASSWORD=root 46 | - MYSQL_DATABASE=thread-testing 47 | - MYSQL_USER=user 48 | - MYSQL_PASSWORD=secret 49 | 50 | beanstalkd: 51 | image: schickling/beanstalkd 52 | container_name: thread-beanstalkd 53 | -------------------------------------------------------------------------------- /backend/docker/mysql/mysql.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | default-authentication-plugin=mysql_native_password 3 | 4 | -------------------------------------------------------------------------------- /backend/docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | root /var/www/app/public; 5 | 6 | index index.php index.htm index.html; 7 | 8 | charset utf-8; 9 | 10 | location / { 11 | try_files $uri $uri/ /index.php?$query_string; 12 | } 13 | 14 | location = /favicon.ico { access_log off; log_not_found off; } 15 | location = /robots.txt { access_log off; log_not_found off; } 16 | 17 | client_max_body_size 108M; 18 | 19 | access_log off; 20 | error_log /var/log/nginx/application.error.log error; 21 | 22 | location ~ \.php$ { 23 | # php-fpm container url 24 | fastcgi_pass app:9000; 25 | fastcgi_index index.php; 26 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 27 | fastcgi_buffers 16 16k; 28 | fastcgi_buffer_size 32k; 29 | include fastcgi_params; 30 | } 31 | 32 | location ~ /\.ht { 33 | deny all; 34 | } 35 | } -------------------------------------------------------------------------------- /backend/docker/php-fpm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0.13-fpm-bullseye 2 | 3 | # Fix debconf warnings upon build 4 | ARG DEBIAN_FRONTEND=noninteractive 5 | 6 | # Fix permissions 7 | RUN usermod -u 1000 www-data \ 8 | && groupmod -g 1000 www-data 9 | 10 | # Install selected extensions and other stuff 11 | RUN apt-get update \ 12 | && apt-get -y --no-install-recommends install \ 13 | ssmtp \ 14 | mailutils \ 15 | apt-utils \ 16 | libpq-dev \ 17 | libfreetype6-dev \ 18 | libjpeg62-turbo-dev \ 19 | libpng-dev \ 20 | zip \ 21 | unzip \ 22 | && docker-php-ext-install pdo_mysql bcmath sockets zip \ 23 | && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \ 24 | && docker-php-ext-install gd \ 25 | && apt-get clean; rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* 26 | 27 | RUN pecl channel-update pecl.php.net \ 28 | && pecl install redis-5.3.4 \ 29 | && pecl install xdebug-3.1.1 \ 30 | && docker-php-ext-enable redis xdebug 31 | 32 | COPY --from=composer /usr/bin/composer /usr/bin/composer 33 | 34 | # Copy xdebug configuration for remote debugging 35 | COPY ./xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini 36 | 37 | -------------------------------------------------------------------------------- /backend/docker/php-fpm/php-ini-overrides.ini: -------------------------------------------------------------------------------- 1 | upload_max_filesize = 100M 2 | post_max_size = 108M 3 | sendmail_path = /usr/sbin/sendmail -t -i -------------------------------------------------------------------------------- /backend/docker/php-fpm/xdebug.ini: -------------------------------------------------------------------------------- 1 | xdebug.mode=debug 2 | xdebug.log_level=0 3 | xdebug.start_with_request=yes 4 | xdebug.discover_client_host=1 5 | -------------------------------------------------------------------------------- /backend/heroku.nginx.conf: -------------------------------------------------------------------------------- 1 | location / { 2 | # try to serve file directly, fallback to rewrite 3 | try_files $uri @rewriteapp; 4 | } 5 | 6 | location @rewriteapp { 7 | # rewrite all to app.php 8 | rewrite ^(.*)$ /index.php$1 last; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "mix", 6 | "watch": "mix watch", 7 | "watch-poll": "mix watch -- --watch-options-poll=1000", 8 | "hot": "mix watch --hot", 9 | "prod": "npm run production", 10 | "production": "mix --production" 11 | }, 12 | "devDependencies": { 13 | "axios": "^0.21.2", 14 | "laravel-mix": "^6.0.6", 15 | "lodash": "^4.17.19", 16 | "postcss": "^8.1.14" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/phpunit.travis.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /backend/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /backend/public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /backend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryStudioAcademy/thread-php/c138d6b475265bd671ffdfdea667ded658725951/backend/public/favicon.ico -------------------------------------------------------------------------------- /backend/public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /backend/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /backend/public/vendor/telescope/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryStudioAcademy/thread-php/c138d6b475265bd671ffdfdea667ded658725951/backend/public/vendor/telescope/favicon.ico -------------------------------------------------------------------------------- /backend/public/vendor/telescope/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=28ad82eca4abdd3f28ec", 3 | "/app-dark.css": "/app-dark.css?id=3ae28ef5f7b987d68dc6", 4 | "/app.css": "/app.css?id=7c970f699ed9cf60d80b" 5 | } 6 | -------------------------------------------------------------------------------- /backend/public/web.config: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /backend/resources/js/app.js: -------------------------------------------------------------------------------- 1 | require('./bootstrap'); 2 | -------------------------------------------------------------------------------- /backend/resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | 2 | window._ = require('lodash'); 3 | 4 | /** 5 | * We'll load the axios HTTP library which allows us to easily issue requests 6 | * to our Laravel back-end. This library automatically handles sending the 7 | * CSRF token as a header based on the value of the "XSRF" token cookie. 8 | */ 9 | 10 | window.axios = require('axios'); 11 | 12 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 13 | 14 | /** 15 | * Next we will register the CSRF Token as a common header with Axios so that 16 | * all outgoing HTTP requests automatically have it attached. This is just 17 | * a simple convenience so we don't have to attach every token manually. 18 | */ 19 | 20 | let token = document.head.querySelector('meta[name="csrf-token"]'); 21 | 22 | if (token) { 23 | window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 24 | } else { 25 | console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); 26 | } 27 | 28 | /** 29 | * Echo exposes an expressive API for subscribing to channels and listening 30 | * for events that are broadcast by Laravel. Echo and event broadcasting 31 | * allows your team to easily build robust real-time web applications. 32 | */ 33 | 34 | // import Echo from 'laravel-echo' 35 | 36 | // window.Pusher = require('pusher-js'); 37 | 38 | // window.Echo = new Echo({ 39 | // broadcaster: 'pusher', 40 | // key: process.env.MIX_PUSHER_APP_KEY, 41 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 42 | // forceTLS: true 43 | // }); 44 | -------------------------------------------------------------------------------- /backend/resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /backend/resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /backend/resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /backend/resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /backend/resources/views/emails/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome! 6 | 7 | 8 | Welcome to Thread application! 9 | 10 | 11 | -------------------------------------------------------------------------------- /backend/routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 22 | Route::group(['prefix' => 'auth'], function () { 23 | Route::post('/register', [AuthController::class, 'register']); 24 | Route::post('/login', [AuthController::class, 'login']); 25 | Route::get('/me', [AuthController::class, 'me']); 26 | Route::put('/me', [AuthController::class, 'update']); 27 | Route::post('/me/image', [AuthController::class, 'uploadProfileImage']); 28 | Route::post('/logout', [AuthController::class, 'logout']); 29 | }); 30 | 31 | Route::group(['middleware' => 'auth:api'], function () { 32 | Route::group(['prefix' => '/users'], function () { 33 | Route::get('/', [UserController::class, 'getUserCollection']); 34 | Route::get('/{id}', [UserController::class, 'getUserById']); 35 | Route::get('/{id}/tweets', [TweetController::class, 'getTweetCollectionByUserId']); 36 | }); 37 | 38 | Route::group(['prefix' => '/tweets'], function () { 39 | Route::get('/', [TweetController::class, 'getTweetCollection']); 40 | Route::post('/', [TweetController::class, 'addTweet']); 41 | Route::get('/{id}', [TweetController::class, 'getTweetById']); 42 | Route::get('/{id}/comments', [CommentController::class, 'getCommentCollectionByTweetId']); 43 | Route::post('/{id}/image', [TweetController::class, 'uploadTweetImage']); 44 | Route::put('/{id}', [TweetController::class, 'updateTweetById']); 45 | Route::delete('/{id}', [TweetController::class, 'deleteTweetById']); 46 | Route::put('/{id}/like', [LikeController::class, 'likeOrDislikeTweet']); 47 | }); 48 | 49 | Route::group(['prefix' => '/comments'], function () { 50 | Route::get('/', [CommentController::class, 'getCommentCollection']); 51 | Route::get('/{id}', [CommentController::class, 'getCommentById']); 52 | Route::post('/', [CommentController::class, 'newComment']); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /backend/routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | 20 | Broadcast::channel('tweets', function () { 21 | return true; 22 | }); 23 | -------------------------------------------------------------------------------- /backend/routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /backend/routes/web.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__ . '/public' . $uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__ . '/public/index.php'; 22 | -------------------------------------------------------------------------------- /backend/storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /backend/storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /backend/storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /backend/storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/tests/Feature/Api/V1/CommentApiTest.php: -------------------------------------------------------------------------------- 1 | actingWithToken()->assertCollectionResponse(self::API_URL, self::COMMENT_RESOURCE_STRUCTURE); 18 | } 19 | 20 | public function test_get_comment_collection_invalid_query_params() 21 | { 22 | $this->actingWithToken()->assertCollectionErrorResponse(self::API_URL, ['page' => 0]); 23 | } 24 | 25 | public function test_get_comment_by_id() 26 | { 27 | $comment = Comment::first(); 28 | 29 | $this->actingWithToken() 30 | ->assertItemResponse( 31 | $this->createResourceItemUri(self::API_URL, $comment->id), 32 | self::COMMENT_RESOURCE_STRUCTURE 33 | ); 34 | } 35 | 36 | public function test_get_comment_by_id_not_found() 37 | { 38 | $this->actingWithToken()->assertNotFoundResponse($this->createResourceItemUri(self::API_URL, 999)); 39 | } 40 | 41 | public function test_add_comment() 42 | { 43 | $tweet = Tweet::first(); 44 | 45 | $this->actingWithToken() 46 | ->assertCreatedResponse(self::API_URL, [ 47 | 'body' => 'Text', 48 | 'tweet_id' => $tweet->id 49 | ]); 50 | } 51 | 52 | /** 53 | * Comment body, id request params 54 | * 55 | * Should be public 56 | * 57 | * @return array 58 | */ 59 | public function commentInvalidAttributesProvider(): array 60 | { 61 | return [ 62 | [ 63 | '', 9999 64 | ], 65 | [ 66 | 'text', 0 67 | ] 68 | ]; 69 | } 70 | 71 | /** 72 | * @dataProvider commentInvalidAttributesProvider 73 | * @param string $body 74 | * @param int $id 75 | */ 76 | public function test_add_comment_invalid_request_params(string $body, int $id) 77 | { 78 | $this->actingWithToken() 79 | ->assertErrorResponse(self::API_URL, [ 80 | 'body' => $body, 81 | 'tweet_id' => $id 82 | ]); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /backend/tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the CSS 10 | | file for the applications as well as bundling up all the JS files. 11 | | 12 | */ 13 | 14 | mix.js('resources/js/app.js', 'public/js') 15 | .postCss('resources/css/app.css', 'public/css', [ 16 | // 17 | ]); 18 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 4 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 120 8 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | VUE_APP_API_URL=http://localhost:7777/api/v1 2 | VUE_APP_PUSHER_APP_KEY= 3 | VUE_APP_PUSHER_APP_CLUSTER=eu 4 | VUE_APP_PUSHER_APP_AUTH_ENDPOINT=http://localhost:7777/broadcasting/auth 5 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/strongly-recommended', 8 | '@vue/airbnb', 9 | ], 10 | rules: { 11 | 'no-console': 'error', 12 | 'no-debugger': 'error', 13 | 'vue/html-indent': ['error', 4], 14 | 'vue/max-attributes-per-line': ['error', { 15 | singleline: 3, 16 | multiline: { 17 | max: 1, 18 | allowFirstLine: false, 19 | }, 20 | }], 21 | indent: [ 22 | 'error', 23 | 4, 24 | ], 25 | 'linebreak-style': [ 26 | 'error', 27 | 'unix', 28 | ], 29 | quotes: [ 30 | 'error', 31 | 'single', 32 | ], 33 | semi: [ 34 | 'error', 35 | 'always', 36 | ], 37 | 'arrow-parens': 'off', 38 | 'no-param-reassign': 'off', 39 | 'vue/singleline-html-element-content-newline': 'off', 40 | 'comma-dangle': 'off', 41 | 'import/prefer-default-export': 'off', 42 | 'max-len': ['error', { 43 | code: 120, 44 | tabWidth: 4 45 | }], 46 | 'no-trailing-spaces': 'off', 47 | 'no-plusplus': 'off' 48 | }, 49 | parserOptions: { 50 | parser: 'babel-eslint', 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | 23 | package-lock.json 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # BSA Mini-Project - Thread (frontend) 2 | 3 | ## Technologies 4 | 5 | * [Vue.js](https://vuejs.org/) framework 6 | * [vue-cli](https://cli.vuejs.org/) 7 | * [vue-router](https://router.vuejs.org/) 8 | * [vuex](https://vuex.vuejs.org/) 9 | * [Buefy](https://buefy.org/) UI framework 10 | * [pusher-js](https://github.com/pusher/pusher-js) 11 | 12 | ## Getting started 13 | 14 | Install the following packages prior to standing up your development environment: 15 | 16 | - [node](https://nodejs.org/en/) 17 | - [yarn](https://yarnpkg.com/en/docs/install) 18 | 19 | Prepare environment: 20 | ``` 21 | cp .env.example .env.local 22 | yarn install 23 | ``` 24 | 25 | Configure your `.env.local`: 26 | ``` 27 | VUE_APP_API_URL=http://:/api/v1 28 | VUE_APP_PUSHER_APP_KEY= 29 | VUE_APP_PUSHER_APP_CLUSTER= 30 | VUE_APP_PUSHER_APP_AUTH_ENDPOINT=http://:/broadcasting/auth 31 | ``` 32 | 33 | Replace `` and `` according to your backend URL. You should register an account and create an application on [Pusher](https://pusher.com/) site to get `` and `` variables. These credentials also should be used on the backend. 34 | 35 | After that application is ready. You can run it in development mode with `yarn run serve` or `yarn run build` to generate compiled and minified files. 36 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "thread", 4 | "build": { 5 | "env": { 6 | "VUE_APP_API_URL": "@api-url", 7 | "VUE_APP_PUSHER_APP_KEY": "@pusher-app-key", 8 | "VUE_APP_PUSHER_APP_CLUSTER": "@pusher-app-cluster", 9 | "VUE_APP_PUSHER_APP_AUTH_ENDPOINT": "@pusher-app-auth-endpoint" 10 | } 11 | }, 12 | "builds": [{ "src": "package.json", "use": "@now/static-build" }], 13 | "routes": [ 14 | { "src": "^/js/(.*)", "dest": "/js/$1" }, 15 | { "src": "^/css/(.*)", "dest": "/css/$1" }, 16 | { "src": "^/img/(.*)", "dest": "/img/$1" }, 17 | { "src": ".*", "dest": "/index.html" } 18 | ] 19 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "now-build": "vue-cli-service build" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 13 | "@fortawesome/free-brands-svg-icons": "^5.13.0", 14 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 15 | "@fortawesome/vue-fontawesome": "^0.1.9", 16 | "axios": "^0.21.3", 17 | "buefy": "^0.8.18", 18 | "moment": "^2.25.3", 19 | "pusher-js": "^6.0.3", 20 | "vue": "^2.6.11", 21 | "vue-infinite-loading": "^2.4.5", 22 | "vue-router": "^3.1.6", 23 | "vuex": "^3.4.0" 24 | }, 25 | "devDependencies": { 26 | "@vue/cli-plugin-babel": "^4.3.1", 27 | "@vue/cli-plugin-eslint": "^4.3.1", 28 | "@vue/cli-service": "^4.3.1", 29 | "@vue/eslint-config-airbnb": "^5.0.2", 30 | "babel-eslint": "^10.1.0", 31 | "eslint": "^7.0.0", 32 | "eslint-plugin-vue": "^6.2.2", 33 | "sass": "^1.26.5", 34 | "sass-loader": "^8.0.2", 35 | "vue-template-compiler": "^2.6.11" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryStudioAcademy/thread-php/c138d6b475265bd671ffdfdea667ded658725951/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Thread 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 51 | 52 | 56 | -------------------------------------------------------------------------------- /frontend/src/api/Api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios/index'; 2 | import Storage from '@/services/Storage'; 3 | import { EventEmitter, TOKEN_EXPIRED_EVENT } from '@/services/EventEmitter'; 4 | import { UNAUTHENTICATED } from '@/api/ErrorCodes'; 5 | import { getSocketId } from '@/services/Pusher'; 6 | 7 | class Api { 8 | constructor(apiUrl, authHeaderName = 'Authorization') { 9 | this.axios = axios.create({ baseURL: apiUrl }); 10 | 11 | this.axios 12 | .interceptors 13 | .request 14 | .use( 15 | config => { 16 | if (Storage.hasToken()) { 17 | config.headers[authHeaderName] = `${Storage.getTokenType()} ${Storage.getToken()}`; 18 | } 19 | 20 | if (getSocketId()) { 21 | config.headers['X-Socket-ID'] = getSocketId(); 22 | } 23 | 24 | return config; 25 | }, 26 | error => Promise.reject(error) 27 | ); 28 | 29 | this.axios 30 | .interceptors 31 | .response 32 | .use( 33 | response => response.data.data, 34 | errorResponse => { 35 | const { response } = errorResponse; 36 | 37 | if (!response) { 38 | return Promise.reject(new Error('Unexpected error!')); 39 | } 40 | 41 | const error = response.data.errors[0]; 42 | 43 | if (error.code === UNAUTHENTICATED) { 44 | EventEmitter.$emit(TOKEN_EXPIRED_EVENT, error); 45 | } 46 | 47 | return Promise.reject(error); 48 | }, 49 | ); 50 | } 51 | 52 | get(url, params) { 53 | return this.axios.get(url, { params }); 54 | } 55 | 56 | post(url, data) { 57 | return this.axios.post(url, data); 58 | } 59 | 60 | put(url, data) { 61 | return this.axios.put(url, data); 62 | } 63 | 64 | delete(url, params) { 65 | return this.axios.delete(url, params); 66 | } 67 | } 68 | 69 | export default new Api(process.env.VUE_APP_API_URL); 70 | -------------------------------------------------------------------------------- /frontend/src/api/ErrorCodes.js: -------------------------------------------------------------------------------- 1 | export const UNAUTHENTICATED = 'unauthenticated'; 2 | -------------------------------------------------------------------------------- /frontend/src/components/common/DefaultAvatar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/common/NoContent.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/common/TweetPreviewList.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 50 | 51 | 56 | -------------------------------------------------------------------------------- /frontend/src/components/filter/filters.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | const createdDate = value => { 4 | if (!value) { 5 | return ''; 6 | } 7 | 8 | return moment.utc(value).fromNow(); 9 | }; 10 | 11 | const createFilters = (Vue) => { 12 | Vue.filter('createdDate', createdDate); 13 | }; 14 | 15 | export default createFilters; 16 | -------------------------------------------------------------------------------- /frontend/src/components/mixin/showStatusToast.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | showErrorMessage(message) { 4 | this.$buefy.toast.open({ 5 | message, 6 | type: 'is-danger', 7 | }); 8 | }, 9 | 10 | showSuccessMessage(message) { 11 | this.$buefy.toast.open({ 12 | message, 13 | type: 'is-success', 14 | }); 15 | }, 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/components/view/feed/NewTweetForm.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 82 | 83 | 88 | -------------------------------------------------------------------------------- /frontend/src/components/view/tweet/Comment.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 52 | 53 | 64 | -------------------------------------------------------------------------------- /frontend/src/components/view/tweet/EditTweetForm.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 86 | 87 | 92 | -------------------------------------------------------------------------------- /frontend/src/components/view/tweet/NewCommentForm.vue: -------------------------------------------------------------------------------- 1 |