├── resources
├── css
│ └── app.css
├── js
│ ├── components
│ │ ├── EventBus.js
│ │ ├── Notifications.vue
│ │ ├── MediaUploader.vue
│ │ └── PostCreator.vue
│ ├── app.js
│ └── bootstrap.js
├── sass
│ ├── app.scss
│ └── _variables.scss
├── views
│ ├── main
│ │ ├── media.blade.php
│ │ ├── calendar.blade.php
│ │ ├── posts
│ │ │ ├── create.blade.php
│ │ │ ├── edit.blade.php
│ │ │ └── index.blade.php
│ │ ├── dashboard.blade.php
│ │ └── notifications.blade.php
│ ├── auth
│ │ ├── verify.blade.php
│ │ ├── passwords
│ │ │ ├── confirm.blade.php
│ │ │ ├── email.blade.php
│ │ │ └── reset.blade.php
│ │ ├── login.blade.php
│ │ └── register.blade.php
│ └── layouts
│ │ ├── pagination.blade.php
│ │ └── auth.blade.php
└── lang
│ └── en
│ ├── pagination.php
│ ├── auth.php
│ └── passwords.php
├── bootstrap
├── cache
│ └── .gitignore
└── app.php
├── storage
├── logs
│ └── .gitignore
├── debugbar
│ └── .gitignore
├── framework
│ ├── testing
│ │ └── .gitignore
│ ├── views
│ │ └── .gitignore
│ ├── cache
│ │ ├── data
│ │ │ └── .gitignore
│ │ └── .gitignore
│ ├── sessions
│ │ └── .gitignore
│ └── .gitignore
└── app
│ └── .gitignore
├── database
├── .gitignore
├── seeders
│ └── DatabaseSeeder.php
├── migrations
│ ├── 2020_10_31_133920_create_sessions_pub_id_column.php
│ ├── 2014_10_12_100000_create_password_resets_table.php
│ ├── 2020_10_27_110614_create_media_table.php
│ ├── 2021_02_28_020647_create_notifications_table.php
│ ├── 2019_08_19_000000_create_failed_jobs_table.php
│ ├── 2020_10_30_110207_create_social_auth_users_table.php
│ ├── 2014_10_12_000000_create_users_table.php
│ ├── 2021_02_20_081434_create_jobs_table.php
│ ├── 2021_02_17_140216_create_media_post_table.php
│ ├── 2021_02_24_190312_create_account_post_table.php
│ ├── 0000_00_00_000000_create_websockets_statistics_entries_table.php
│ ├── 2020_10_31_133916_create_sessions_table.php
│ ├── 2020_11_04_123546_create_accounts_table.php
│ └── 2020_11_12_031714_create_posts_table.php
└── factories
│ └── UserFactory.php
├── public
├── robots.txt
├── storage
├── favicon.ico
├── images
│ ├── logo.png
│ └── avatar.png
├── mix-manifest.json
├── webfonts
│ ├── fa-solid-900.eot
│ ├── fa-solid-900.ttf
│ ├── fa-brands-400.eot
│ ├── fa-brands-400.ttf
│ ├── fa-brands-400.woff
│ ├── fa-regular-400.eot
│ ├── fa-regular-400.ttf
│ ├── fa-solid-900.woff
│ ├── fa-solid-900.woff2
│ ├── fa-brands-400.woff2
│ ├── fa-regular-400.woff
│ └── fa-regular-400.woff2
├── .htaccess
├── web.config
└── index.php
├── screenshots
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
├── 9.png
├── 10.png
├── 11.png
└── logo.png
├── .gitattributes
├── tests
├── TestCase.php
├── CreatesApplication.php
└── Feature
│ └── AuthTest.php
├── .styleci.yml
├── .editorconfig
├── .gitignore
├── app
├── Policies
│ ├── SessionPolicy.php
│ ├── AccountPolicy.php
│ ├── MediaPolicy.php
│ ├── NotificationPolicy.php
│ └── PostPolicy.php
├── Models
│ ├── SocialAuthUser.php
│ ├── Session.php
│ ├── Notification.php
│ ├── Casts
│ │ └── DiffForHumans.php
│ ├── Account.php
│ ├── Media.php
│ ├── User.php
│ └── Post.php
├── Http
│ ├── Middleware
│ │ ├── VerifyCsrfToken.php
│ │ ├── EncryptCookies.php
│ │ ├── TrimStrings.php
│ │ ├── TrustHosts.php
│ │ ├── PreventRequestsDuringMaintenance.php
│ │ ├── TrustProxies.php
│ │ ├── Authenticate.php
│ │ └── RedirectIfAuthenticated.php
│ ├── Controllers
│ │ ├── Controller.php
│ │ ├── DashboardController.php
│ │ ├── Auth
│ │ │ ├── ForgotPasswordController.php
│ │ │ ├── ResetPasswordController.php
│ │ │ ├── LoginController.php
│ │ │ ├── ConfirmPasswordController.php
│ │ │ ├── VerificationController.php
│ │ │ └── RegisterController.php
│ │ ├── AccountController.php
│ │ ├── SocialAuthController.php
│ │ ├── NotificationController.php
│ │ ├── MediaController.php
│ │ ├── ProfileController.php
│ │ └── PostController.php
│ ├── Requests
│ │ ├── MediaRequest.php
│ │ ├── ProfilePasswordRequest.php
│ │ ├── ProfileInfoRequest.php
│ │ └── PostRequest.php
│ └── Kernel.php
├── PostaBot
│ ├── Facades
│ │ └── Publisher.php
│ ├── Exceptions
│ │ └── TokenizerException.php
│ ├── Contracts
│ │ └── Tokenizable.php
│ ├── PublisherManager.php
│ ├── TokenizerProviders
│ │ └── Twitter.php
│ └── PublisherProviders
│ │ ├── Facebook.php
│ │ └── Twitter.php
├── Exceptions
│ └── Handler.php
├── Rules
│ ├── CheckOldPass.php
│ ├── PostMediaCheck.php
│ └── PostAccountsCheck.php
├── Providers
│ ├── BroadcastServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── EventServiceProvider.php
│ ├── PostaBotServiceProvider.php
│ ├── AppServiceProvider.php
│ └── RouteServiceProvider.php
├── Events
│ └── PostPublishEvent.php
├── Listeners
│ ├── SuccessfulLoginListener.php
│ └── PostPublishListener.php
├── Jobs
│ └── PublishPost.php
├── Console
│ └── Kernel.php
├── Services
│ ├── ProfileService.php
│ ├── MediaService.php
│ ├── PostService.php
│ └── SocialAuthService.php
├── Notifications
│ ├── LoginNotification.php
│ └── PostNotification.php
└── Repositories
│ └── UserRepository.php
├── webpack.mix.js
├── routes
├── channels.php
├── api.php
├── console.php
└── web.php
├── server.php
├── config
├── cors.php
├── view.php
├── hashing.php
├── services.php
├── broadcasting.php
├── filesystems.php
├── queue.php
├── logging.php
├── cache.php
├── mail.php
└── auth.php
├── .env.example
├── phpunit.xml
├── package.json
├── artisan
├── README.md
└── composer.json
/resources/css/app.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite
2 | *.sqlite-journal
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/storage/debugbar/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/testing/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !/public/profile/default.png
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/public/storage:
--------------------------------------------------------------------------------
1 | /media/veracrypt1/Dev/finished-projects/laravel-laposta/storage/app/public
--------------------------------------------------------------------------------
/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/1.png
--------------------------------------------------------------------------------
/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/2.png
--------------------------------------------------------------------------------
/screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/3.png
--------------------------------------------------------------------------------
/screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/4.png
--------------------------------------------------------------------------------
/screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/5.png
--------------------------------------------------------------------------------
/screenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/6.png
--------------------------------------------------------------------------------
/screenshots/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/7.png
--------------------------------------------------------------------------------
/screenshots/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/8.png
--------------------------------------------------------------------------------
/screenshots/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/9.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/screenshots/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/10.png
--------------------------------------------------------------------------------
/screenshots/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/11.png
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/images/logo.png
--------------------------------------------------------------------------------
/screenshots/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/screenshots/logo.png
--------------------------------------------------------------------------------
/public/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/images/avatar.png
--------------------------------------------------------------------------------
/public/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/js/app.js": "/js/app.js",
3 | "/css/app.css": "/css/app.css"
4 | }
5 |
--------------------------------------------------------------------------------
/public/webfonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/public/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/resources/js/components/EventBus.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 |
3 | const Bus = new Vue({});
4 |
5 | export default Bus;
6 |
--------------------------------------------------------------------------------
/public/webfonts/fa-brands-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-brands-400.eot
--------------------------------------------------------------------------------
/public/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/public/webfonts/fa-brands-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-brands-400.woff
--------------------------------------------------------------------------------
/public/webfonts/fa-regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-regular-400.eot
--------------------------------------------------------------------------------
/public/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/public/webfonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/public/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/public/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/public/webfonts/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-regular-400.woff
--------------------------------------------------------------------------------
/public/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/civilcoder55/laravel-laposta/HEAD/public/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.css linguist-vendored
3 | *.scss linguist-vendored
4 | *.js linguist-vendored
5 | CHANGELOG.md export-ignore
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/sass/app.scss:
--------------------------------------------------------------------------------
1 | // Fonts
2 | @import url('https://fonts.googleapis.com/css?family=Nunito');
3 |
4 | // Variables
5 | @import 'variables';
6 |
7 | // Bootstrap
8 | @import '~bootstrap/scss/bootstrap';
9 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | id == $session->user_id;
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | create();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Policies/AccountPolicy.php:
--------------------------------------------------------------------------------
1 | id == $account->user_id;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/Policies/MediaPolicy.php:
--------------------------------------------------------------------------------
1 | id == $media->user_id;
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/app/Models/SocialAuthUser.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Http/Middleware/VerifyCsrfToken.php:
--------------------------------------------------------------------------------
1 | 'string',
18 | ];
19 | }
20 |
--------------------------------------------------------------------------------
/app/PostaBot/Facades/Publisher.php:
--------------------------------------------------------------------------------
1 | error = $error;
13 | }
14 | public function render()
15 | {
16 | return redirect()->route('accounts.index')->with('error', $this->error);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustHosts.php:
--------------------------------------------------------------------------------
1 | allSubdomainsOfApplicationUrl(),
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/Http/Middleware/PreventRequestsDuringMaintenance.php:
--------------------------------------------------------------------------------
1 | user()->password);
15 | }
16 |
17 | public function message()
18 | {
19 | return 'You entered wrong password ';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/Models/Notification.php:
--------------------------------------------------------------------------------
1 | 'array',
15 | 'read_at' => 'datetime',
16 | 'created_at' => DiffForHumans::class,
17 | ];
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
19 |
20 | return $app;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Http/Requests/MediaRequest.php:
--------------------------------------------------------------------------------
1 | ['required', 'array', 'min:1'],
18 | 'media.*' => ['required', 'image', 'mimes:jpeg,png,jpg'],
19 | ];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/Providers/BroadcastServiceProvider.php:
--------------------------------------------------------------------------------
1 | diffForHumans();
14 | }
15 |
16 | public function set($model, string $key, $value, array $attributes)
17 | {
18 |
19 | return $value;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/Http/Requests/ProfilePasswordRequest.php:
--------------------------------------------------------------------------------
1 | ['required', new CheckOldPass],
19 | 'password' => ['required', 'min:6', 'confirmed'],
20 | ];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Http/Controllers/DashboardController.php:
--------------------------------------------------------------------------------
1 | ['unique:users,email,' . auth()->user()->id, 'required', 'email'],
19 | 'name' => ['required', 'string'],
20 | '_avatar' => ['image'],
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Policies/NotificationPolicy.php:
--------------------------------------------------------------------------------
1 | id == $notification->notifiable_id;
16 | }
17 |
18 | public function delete(User $user, Notification $notification)
19 | {
20 | return $user->id == $notification->notifiable_id;
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Authenticate.php:
--------------------------------------------------------------------------------
1 | expectsJson()) {
19 | return route('login');
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Events/PostPublishEvent.php:
--------------------------------------------------------------------------------
1 | post = $post;
19 | $this->account = $account;
20 | $this->error = $error;
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/resources/views/main/media.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.app')
2 | @section('title', 'Media')
3 | @section('content')
4 |
17 | @endsection
--------------------------------------------------------------------------------
/app/Models/Account.php:
--------------------------------------------------------------------------------
1 | belongsTo('App\User');
23 | }
24 |
25 | public function posts()
26 | {
27 | return $this->belongsToMany(Post::class);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/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 Sass
10 | | file for the application as well as bundling up all the JS files.
11 | |
12 | */
13 |
14 | mix.js("resources/js/app.js", "public/js")
15 | .vue()
16 | .sass("resources/sass/app.scss", "public/css");
17 |
--------------------------------------------------------------------------------
/app/Providers/AuthServiceProvider.php:
--------------------------------------------------------------------------------
1 | registerPolicies();
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/routes/channels.php:
--------------------------------------------------------------------------------
1 | id === (int) $id;
18 | });
19 |
--------------------------------------------------------------------------------
/resources/lang/en/pagination.php:
--------------------------------------------------------------------------------
1 | '« Previous',
17 | 'next' => 'Next »',
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | get('/user', function (Request $request) {
18 | return $request->user();
19 | });
20 |
--------------------------------------------------------------------------------
/server.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 |
--------------------------------------------------------------------------------
/app/Policies/PostPolicy.php:
--------------------------------------------------------------------------------
1 | id == $post->user_id;
16 | }
17 |
18 | public function edit(User $user, Post $post)
19 | {
20 | return $user->id == $post->user_id && $post->locked == 0;
21 | }
22 |
23 | public function delete(User $user, Post $post)
24 | {
25 | return $user->id == $post->user_id;
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/PostaBot/PublisherManager.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
19 | })->purpose('Display an inspiring quote');
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Rules/PostMediaCheck.php:
--------------------------------------------------------------------------------
1 | get();
15 | foreach ($media as $m) {
16 | if ($m->user_id != auth()->user()->id) {
17 | return false;
18 | }
19 | }
20 | return true;
21 | }
22 |
23 |
24 | public function message()
25 | {
26 | return 'Malformed Request , Please try again .. ';
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/resources/lang/en/auth.php:
--------------------------------------------------------------------------------
1 | 'These credentials do not match our records.',
17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | require("./bootstrap");
2 |
3 | import Vue from "vue";
4 | import VueObserveVisibility from "vue-observe-visibility";
5 | import VueToast from "vue-toast-notification";
6 | import "vue-toast-notification/dist/theme-sugar.css";
7 |
8 | Vue.component(
9 | "MediaUploader",
10 | require("./components/MediaUploader.vue").default
11 | );
12 |
13 | Vue.component("PostCreator", require("./components/PostCreator.vue").default);
14 | Vue.component("PostEditor", require("./components/PostEditor.vue").default);
15 | Vue.component(
16 | "Notifications",
17 | require("./components/Notifications.vue").default
18 | );
19 |
20 | Vue.use(VueObserveVisibility);
21 | Vue.use(VueToast);
22 | const app = new Vue({
23 | el: "#app"
24 | });
25 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/ForgotPasswordController.php:
--------------------------------------------------------------------------------
1 | redirect();
13 | }
14 |
15 | public function getAndSaveData()
16 | {
17 | $user = Socialite::driver('twitter')->user();
18 | auth()->user()->accounts()->updateOrCreate(
19 | ['platform' => 'Twitter', 'uid' => $user->id],
20 | ['name' => $user->nickname, 'type' => 'Account', 'token' => $user->token, 'secret' => $user->tokenSecret]
21 | );
22 | }
23 |
24 | public function revoke($accessToken)
25 | {
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Rules/PostAccountsCheck.php:
--------------------------------------------------------------------------------
1 | get();
16 | if (!request()->draft && $accounts->count() == 0) {
17 | return false;
18 | }
19 | foreach ($accounts as $a) {
20 | if ($a->user_id != auth()->user()->id) {
21 | return false;
22 | }
23 | }
24 | return true;
25 | }
26 |
27 |
28 | public function message()
29 | {
30 | return 'Malformed Request , Please try again .. ';
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/database/migrations/2020_10_31_133920_create_sessions_pub_id_column.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 |
--------------------------------------------------------------------------------
/app/Http/Middleware/RedirectIfAuthenticated.php:
--------------------------------------------------------------------------------
1 | check()) {
26 | return redirect(RouteServiceProvider::HOME);
27 | }
28 | }
29 |
30 | return $next($request);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Providers/EventServiceProvider.php:
--------------------------------------------------------------------------------
1 | [
18 | 'App\Listeners\SuccessfulLoginListener',
19 | ],
20 |
21 | PostPublishEvent::class => [
22 | PostPublishListener::class
23 | ],
24 |
25 | ];
26 |
27 | /**
28 | * Register any events for your application.
29 | *
30 | * @return void
31 | */
32 | public function boot()
33 | {
34 | //
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->name,
27 | 'email' => $this->faker->unique()->safeEmail,
28 | 'email_verified_at' => now(),
29 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
30 | 'remember_token' => Str::random(10),
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/database/migrations/2020_10_27_110614_create_media_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->foreignId('user_id')->constrained()->onDelete('cascade');
19 | $table->string('name');
20 | $table->string('original_path');
21 | $table->string('thumb_path');
22 | $table->timestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | *
29 | * @return void
30 | */
31 | public function down()
32 | {
33 | Schema::dropIfExists('media');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/database/migrations/2021_02_28_020647_create_notifications_table.php:
--------------------------------------------------------------------------------
1 | uuid('id')->primary();
18 | $table->string('type');
19 | $table->morphs('notifiable');
20 | $table->text('data');
21 | $table->timestamp('read_at')->nullable();
22 | $table->timestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | *
29 | * @return void
30 | */
31 | public function down()
32 | {
33 | Schema::dropIfExists('notifications');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/Listeners/SuccessfulLoginListener.php:
--------------------------------------------------------------------------------
1 | request = $request;
17 | }
18 |
19 | public function handle(Login $event)
20 | {
21 | //if new registered user , don't send notification
22 | if ($event->user->created_at->timestamp + 1 >= Carbon::now()->timestamp) {
23 | return;
24 | }
25 | $agent = new Parser($this->request->server('HTTP_USER_AGENT'));
26 | $browser = $agent->browser->toString();
27 | $os = $agent->os->toString();
28 | $event->user->notify(new LoginNotification($browser, $os)); // fire LoginNotification which store notification in database and brodcast it
29 |
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/ResetPasswordController.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 |
--------------------------------------------------------------------------------
/app/Models/Media.php:
--------------------------------------------------------------------------------
1 | delete($media->original_path);
22 | Storage::disk('local')->delete($media->thumb_path);
23 | }));
24 |
25 | }
26 |
27 | public function user()
28 | {
29 | return $this->belongsTo('App\User');
30 | }
31 |
32 | public function posts()
33 | {
34 | return $this->belongsToMany(Post::class);
35 | }
36 |
37 | public function getSrcAttribute()
38 | {
39 | return route('media.show.thumb', $this->name);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/Providers/PostaBotServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->singleton(Tokenizable::class, function ($app) {
20 | $platforms = ['facebook' => 'App\PostaBot\TokenizerProviders\Facebook', 'twitter' => 'App\PostaBot\TokenizerProviders\Twitter', 'instagram' => 'App\PostaBot\TokenizerProviders\Instagram'];
21 | $platform = Route::current()->parameter('platform');
22 | return new $platforms[$platform];
23 |
24 | });
25 |
26 | $this->app->singleton(PublisherManager::class, function ($app) {
27 | return new PublisherManager($app);
28 | });
29 |
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/database/migrations/2020_10_30_110207_create_social_auth_users_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->foreignId('user_id')->constrained()->onDelete('cascade');
19 | $table->enum('provider', array('facebook', 'google'));
20 | $table->string('uid')->unique();
21 | $table->string('token');
22 | $table->timestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | *
29 | * @return void
30 | */
31 | public function down()
32 | {
33 | Schema::dropIfExists('social_auth_users');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/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('avatar')->nullable();
23 | $table->rememberToken();
24 | $table->timestamps();
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | *
31 | * @return void
32 | */
33 | public function down()
34 | {
35 | Schema::dropIfExists('users');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2021_02_20_081434_create_jobs_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->string('queue')->index();
19 | $table->longText('payload');
20 | $table->unsignedTinyInteger('attempts');
21 | $table->unsignedInteger('reserved_at')->nullable();
22 | $table->unsignedInteger('available_at');
23 | $table->unsignedInteger('created_at');
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('jobs');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/migrations/2021_02_17_140216_create_media_post_table.php:
--------------------------------------------------------------------------------
1 | id();
18 |
19 | $table->bigInteger('post_id')->unsigned();
20 | $table->bigInteger('media_id')->unsigned();
21 |
22 | $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
23 | $table->foreign('media_id')->references('id')->on('media')->onDelete('cascade');
24 |
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | *
31 | * @return void
32 | */
33 | public function down()
34 | {
35 | Schema::dropIfExists('media_post');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2021_02_24_190312_create_account_post_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->bigInteger('post_id')->unsigned();
19 | $table->bigInteger('account_id')->unsigned();
20 |
21 | $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
22 | $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | *
29 | * @return void
30 | */
31 | public function down()
32 | {
33 | Schema::dropIfExists('account_post');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->string('app_id');
19 | $table->integer('peak_connection_count');
20 | $table->integer('websocket_message_count');
21 | $table->integer('api_message_count');
22 | $table->nullableTimestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | *
29 | * @return void
30 | */
31 | public function down()
32 | {
33 | Schema::dropIfExists('websockets_statistics_entries');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/Providers/AppServiceProvider.php:
--------------------------------------------------------------------------------
1 | rememberForever('notifications.' . auth()->user()->id, function () {
30 | return auth()
31 | ->user()
32 | ->notifications()
33 | ->orderBy('created_at', 'desc')
34 | ->limit(10)
35 | ->get();
36 | });
37 |
38 | $view->with('notifications', $notifications);
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/database/migrations/2020_10_31_133916_create_sessions_table.php:
--------------------------------------------------------------------------------
1 | integer('pub_id')->unique()->index();
20 | $table->string('id')->primary();
21 | $table->foreignId('user_id')->nullable()->index();
22 | $table->string('ip_address', 45)->nullable();
23 | $table->text('user_agent')->nullable();
24 | $table->text('payload');
25 | $table->integer('last_activity')->index();
26 |
27 | });
28 | }
29 |
30 | /**
31 | * Reverse the migrations.
32 | *
33 | * @return void
34 | */
35 | public function down()
36 | {
37 | Schema::dropIfExists('sessions');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Jobs/PublishPost.php:
--------------------------------------------------------------------------------
1 | post = $post;
24 | $this->account = $account;
25 | }
26 |
27 | public function handle()
28 | {
29 | Publisher::driver($this->account->platform)->publishPost($this->post, $this->account);
30 | event(new PostPublishEvent($this->post, $this->account));
31 | }
32 |
33 | public function failed(Exception $exception)
34 | {
35 | $error = $exception->getMessage();
36 | event(new PostPublishEvent($this->post, $this->account, $error));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/database/migrations/2020_11_04_123546_create_accounts_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->foreignId('user_id')->constrained()->onDelete('cascade');
19 | $table->enum('platform', array('facebook', 'twitter', 'instagram'));
20 | $table->string('uid');
21 | $table->string('name');
22 | $table->string('type');
23 | $table->string('token');
24 | $table->string('secret')->nullable();
25 | $table->timestamps();
26 | });
27 | }
28 |
29 | /**
30 | * Reverse the migrations.
31 | *
32 | * @return void
33 | */
34 | public function down()
35 | {
36 | Schema::dropIfExists('accounts');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/LoginController.php:
--------------------------------------------------------------------------------
1 | middleware('guest')->except('logout');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/ConfirmPasswordController.php:
--------------------------------------------------------------------------------
1 | middleware('auth');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/Http/Controllers/AccountController.php:
--------------------------------------------------------------------------------
1 | redirect();
21 | }
22 |
23 | // handling redirection callback form platform and add account to database
24 | public function callback($platform, Tokenizable $Tokenizer)
25 | {
26 | $Tokenizer->getAndSaveData();
27 | return redirect()->route('accounts.index')->with('status', "{$platform} account added successfully");
28 | }
29 |
30 | public function destroy(Account $account)
31 | {
32 | $this->authorize('delete', $account);
33 | $account->delete();
34 | return redirect()->route('accounts.index')->with('status', "{$account->platform} account deleted successfully");
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2020_11_12_031714_create_posts_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->foreignId('user_id')->constrained()->onDelete('cascade');
19 | $table->text('message')->nullable();
20 | $table->boolean('draft')->default(1)->index();
21 | $table->enum('status', array('pending', 'succeeded', 'critical', 'failed'))->default('pending');
22 | $table->json('logs')->nullable();
23 | $table->bigInteger('schedule_date')->nullable()->index();
24 | $table->boolean('locked')->default(0)->index();
25 | $table->timestamps();
26 | });
27 | }
28 |
29 | /**
30 | * Reverse the migrations.
31 | *
32 | * @return void
33 | */
34 | public function down()
35 | {
36 | Schema::dropIfExists('posts');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Console/Kernel.php:
--------------------------------------------------------------------------------
1 | call(function () {
21 | $now = Carbon::now()->timestamp;
22 | $posts = Post::with(['accounts', 'media'])->where(['draft' => 0, 'locked' => 0])
23 | ->whereBetween('schedule_date', [$now + 1, $now + 60])->get();
24 | foreach ($posts as $post) {
25 | foreach ($post->accounts as $account) {
26 | PublishPost::dispatch($post, $account)->delay($post->schedule_date);
27 | }
28 | }
29 | })->everyMinute();
30 |
31 | }
32 |
33 | protected function commands()
34 | {
35 | $this->load(__DIR__ . '/Commands');
36 |
37 | require base_path('routes/console.php');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/resources/views/main/calendar.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.app')
2 |
3 | @section('title', 'Calendar')
4 | @section('stylesheet')
5 |
6 | @endsection
7 | @section('content')
8 |
16 | @endsection
17 |
18 | @section('script')
19 |
20 |
39 | @endsection
--------------------------------------------------------------------------------
/app/Http/Controllers/SocialAuthController.php:
--------------------------------------------------------------------------------
1 | redirect();
15 | }
16 |
17 | public function callback($provider)
18 | {
19 | $data = Socialite::driver($provider)->user();
20 | return SocialAuthService::handleUser($data, $provider);
21 | }
22 |
23 | public function disconnectFacebook()
24 | {
25 | $socialUser = auth()->user()->socialAuthUser()->where(['provider' => 'facebook'])->firstOrFail();
26 | $socialUser->delete();
27 | SocialAuthService::revokeFacebookToken($socialUser->token);
28 | return redirect()->route('profile.index');
29 | }
30 |
31 | public function disconnectGoogle()
32 | {
33 | $socialUser = auth()->user()->socialAuthUser()->where(['provider' => 'google'])->firstOrFail();
34 | $socialUser->delete();
35 | SocialAuthService::revokeGoogleToken($socialUser->token);
36 | return redirect()->route('profile.index');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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_LEVEL=debug
9 |
10 | DB_CONNECTION=mysql
11 | DB_HOST=127.0.0.1
12 | DB_PORT=3306
13 | DB_DATABASE=laravel
14 | DB_USERNAME=root
15 | DB_PASSWORD=
16 |
17 | BROADCAST_DRIVER=pusher
18 | CACHE_DRIVER=file
19 | QUEUE_CONNECTION=database
20 | SESSION_DRIVER=database
21 | SESSION_LIFETIME=35791394
22 |
23 |
24 |
25 | MAIL_MAILER=smtp
26 | MAIL_HOST=smtp.mailtrap.io
27 | MAIL_PORT=2525
28 | MAIL_USERNAME=null
29 | MAIL_PASSWORD=null
30 | MAIL_ENCRYPTION=null
31 | MAIL_FROM_ADDRESS=null
32 | MAIL_FROM_NAME="${APP_NAME}"
33 |
34 |
35 |
36 | PUSHER_APP_ID=
37 | PUSHER_APP_KEY=
38 | PUSHER_APP_SECRET=
39 | PUSHER_APP_CLUSTER=mt1
40 | PUSHER_APP_SCHEME=https
41 |
42 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
43 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
44 |
45 | LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT=
46 | LARAVEL_WEBSOCKETS_SSL_LOCAL_PK=
47 |
48 | FACEBOOK_CLIENT_ID=
49 | FACEBOOK_CLIENT_SECRET=
50 | FACEBOOK_REDIRECT=
51 |
52 |
53 | GOOGLE_CLIENT_ID=
54 | GOOGLE_CLIENT_SECRET=
55 | GOOGLE_REDIRECT=
56 |
57 |
58 |
59 | TWITTER_CLIENT_ID=
60 | TWITTER_CLIENT_SECRET=
61 | TWITTER_REDIRECT=
62 |
63 |
64 |
65 | INSTAGRAM_CLIENT_ID=
66 | INSTAGRAM_CLIENT_SECRET=
67 | INSTAGRAM_REDIRECT=
68 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/VerificationController.php:
--------------------------------------------------------------------------------
1 | middleware('auth');
39 | $this->middleware('signed')->only('verify');
40 | $this->middleware('throttle:6,1')->only('verify', 'resend');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Models/User.php:
--------------------------------------------------------------------------------
1 | 'datetime'];
18 |
19 | public function media()
20 | {
21 | return $this->hasMany(Media::class);
22 | }
23 |
24 | public function socialAuthUser()
25 | {
26 | return $this->hasMany(SocialAuthUser::class);
27 | }
28 |
29 | public function session()
30 | {
31 | return $this->hasMany(Session::class);
32 | }
33 |
34 | public function accounts()
35 | {
36 | return $this->hasMany(Account::class);
37 | }
38 |
39 | public function posts()
40 | {
41 | return $this->hasMany(Post::class);
42 | }
43 |
44 | public function notifications()
45 | {
46 | return $this->morphMany(Notification::class, 'notifiable');
47 | }
48 |
49 | public function receivesBroadcastNotificationsOn()
50 | {
51 | return 'users.' . $this->id;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/resources/views/auth/verify.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.app')
2 |
3 | @section('content')
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | @if (session('resent'))
12 |
13 | {{ __('A fresh verification link has been sent to your email address.') }}
14 |
15 | @endif
16 |
17 | {{ __('Before proceeding, please check your email for a verification link.') }}
18 | {{ __('If you did not receive the email') }},
19 |
25 |
26 |
27 |
28 |
29 |
30 | @endsection
31 |
--------------------------------------------------------------------------------
/resources/views/layouts/pagination.blade.php:
--------------------------------------------------------------------------------
1 | @if ($paginator->hasPages())
2 |
33 | @endif
34 |
--------------------------------------------------------------------------------
/app/Http/Controllers/NotificationController.php:
--------------------------------------------------------------------------------
1 | authorize('read', $notification);
21 | $notification->markAsRead();
22 | Cache::forget('notifications.' . auth()->user()->id);
23 | return response()->json(['success' => true]);
24 | }
25 |
26 | public function destroy(Notification $notification)
27 | {
28 | $this->authorize('delete', $notification);
29 | $notification->delete();
30 | Cache::forget('notifications.' . auth()->user()->id);
31 | return redirect()->route('notifications.index')->with('status', "Notification deleted successfully");
32 | }
33 |
34 | public function destroyAll()
35 | {
36 | auth()->user()->notifications()->delete();
37 | Cache::forget('notifications.' . auth()->user()->id);
38 | return redirect()->route('notifications.index')->with('status', "All notifications deleted successfully");
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/Listeners/PostPublishListener.php:
--------------------------------------------------------------------------------
1 | createLog($event);
14 | User::where(['id' => $event->post->user_id])->first()->notify(new PostNotification($event->post));
15 | }
16 |
17 | private function createLog($event)
18 | {
19 | if ($event->error) {
20 | $log = [[
21 | 'status' => 'danger',
22 | 'message' => "Post can't be published with {$event->account->name} {$event->account->platform} {$event->account->type} reason: $event->error",
23 | ]];
24 | $status = (($event->post->status == 'failed' || $event->post->status == 'pending') ? 'failed' : 'critical');
25 | $event->post->update(['status' => $status, 'locked' => 1, 'logs' => $log]);
26 | } else {
27 | $log = [[
28 | 'status' => 'success',
29 | 'message' => "Post published successfully at {$event->post->schedule_date} to {$event->account->name} {$event->account->platform} {$event->account->type}",
30 | ]];
31 | $status = (($event->post->status == 'succeeded' || $event->post->status == 'pending') ? 'succeeded' : 'critical');
32 | $event->post->update(['status' => $status, 'locked' => 1, 'logs' => $log]);
33 | }
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/resources/views/layouts/auth.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | La Posta | @yield('title')
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 | @yield('content')
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/Services/ProfileService.php:
--------------------------------------------------------------------------------
1 | _avatar->store('profile', 'public');
17 | Image::make(public_path("storage/{$avatarPath}"))->fit(128, 128)->save();
18 | auth()->user()->update(['avatar' => $avatarPath]);
19 | }
20 |
21 | private static function deleteOldAvatar()
22 | {
23 | $oldAvatarPath = auth()->user()->avatar;
24 | Storage::disk('local')->delete("public/{$oldAvatarPath}");
25 | }
26 |
27 | public static function formatSessions($s)
28 | {
29 | return array_map(function ($session) {
30 | $agent = new Parser($session['user_agent']);
31 | $session['browser'] = $agent->browser->toString();
32 | $session['os'] = $agent->os->toString();
33 | $session['device'] = $agent->device->type;
34 | $session['status'] = (Session::getId() == $session['id']) ? 'This session' : '';
35 | $session['last_activity'] = ($session['status'] == 'This session') ? 'Active Now' : Carbon::createFromTimestamp($session['last_activity'])->diffForHumans();
36 | return $session;
37 | }, $s);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Notifications/LoginNotification.php:
--------------------------------------------------------------------------------
1 | message = "New Login From $browser On $os";
19 | $this->link = route('profile.index') . "#sessionsTable";
20 | }
21 |
22 | public function via($notifiable)
23 | {
24 | return ['broadcast', 'database'];
25 | }
26 |
27 | public function toDatabase($notifiable): array
28 | {
29 | Cache::forget('notifications.' . $notifiable->id);
30 | return ['status' => 'warning', 'type' => 'login', 'message' => $this->message, 'link' => $this->link];
31 | }
32 |
33 | public function toBroadcast($notifiable)
34 | {
35 | return (new BroadcastMessage([
36 | 'id' => $this->id,
37 | 'data' => [
38 | 'status' => 'warning',
39 | 'type' => 'login',
40 | 'message' => $this->message,
41 | 'link' => $this->link,
42 | ],
43 | 'created_at' => 'just now',
44 | ]));
45 | }
46 |
47 | public function toArray($notifiable)
48 | {
49 | return [
50 | //
51 | ];
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | ./tests/Unit
10 |
11 |
12 | ./tests/Feature
13 |
14 |
15 |
16 |
17 | ./app
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | window._ = require("lodash");
2 |
3 | /**
4 | * We'll load jQuery and the Bootstrap jQuery plugin which provides support
5 | * for JavaScript based Bootstrap features such as modals and tabs. This
6 | * code may be modified to fit the specific needs of your application.
7 | */
8 |
9 | try {
10 | window.Popper = require("popper.js").default;
11 | window.$ = window.jQuery = require("jquery");
12 |
13 | require("bootstrap");
14 | } catch (e) {}
15 |
16 | /**
17 | * We'll load the axios HTTP library which allows us to easily issue requests
18 | * to our Laravel back-end. This library automatically handles sending the
19 | * CSRF token as a header based on the value of the "XSRF" token cookie.
20 | */
21 |
22 | window.axios = require("axios");
23 |
24 | window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
25 |
26 | /**
27 | * Echo exposes an expressive API for subscribing to channels and listening
28 | * for events that are broadcast by Laravel. Echo and event broadcasting
29 | * allows your team to easily build robust real-time web applications.
30 | */
31 |
32 | import Echo from "laravel-echo";
33 |
34 | window.Pusher = require("pusher-js");
35 |
36 | window.Echo = new Echo({
37 | broadcaster: "pusher",
38 | key: process.env.MIX_PUSHER_APP_KEY,
39 | cluster: process.env.MIX_PUSHER_APP_CLUSTER,
40 | wsHost: window.location.hostname,
41 | disableStats: true,
42 | wsPort: 6001,
43 | wssPort: 6001,
44 | encrypted: true,
45 | forceTLS: true,
46 | enabledTransports: ["ws", "wss"]
47 | });
48 |
--------------------------------------------------------------------------------
/app/Models/Post.php:
--------------------------------------------------------------------------------
1 | 'array',
16 | ];
17 |
18 | public function user()
19 | {
20 | return $this->belongsTo('App\User');
21 | }
22 |
23 | public function accounts()
24 | {
25 | return $this->belongsToMany(Account::class);
26 | }
27 |
28 | public function media()
29 | {
30 | return $this->belongsToMany(Media::class);
31 | }
32 |
33 | public function getAccountsIdsAttribute()
34 | {
35 | return array_map(function ($item) {
36 | return $item['id'];
37 | }, $this->accounts->toArray());
38 | }
39 |
40 | public function getMediaIdsAttribute()
41 | {
42 | // return array_map(function ($item) {
43 | // return $item['id'];
44 | // }, $this->media->toArray());
45 | return $this->media->toArray();
46 | }
47 | public function getScheduleDateAttribute($value)
48 | {
49 |
50 | return $value ? Carbon::createFromTimestamp($value) : null;
51 | }
52 |
53 | public function setLogsAttribute($value)
54 | {
55 | $old = isset($this->attributes['logs']) ? json_decode($this->attributes['logs']) : [];
56 | $new = array_merge($value, $old);
57 | $this->attributes['logs'] = json_encode($new);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "npm run development",
5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --config=node_modules/laravel-mix/setup/webpack.config.js",
6 | "watch": "npm run development -- --watch",
7 | "watch-poll": "npm run watch -- --watch-poll",
8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js",
9 | "prod": "npm run production",
10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --config=node_modules/laravel-mix/setup/webpack.config.js"
11 | },
12 | "devDependencies": {
13 | "axios": "^0.21.1",
14 | "bootstrap": "^4.6.0",
15 | "cross-env": "^7.0",
16 | "jquery": "^3.6",
17 | "laravel-mix": "^6.0.0-beta.17",
18 | "lodash": "^4.17.19",
19 | "popper.js": "^1.16.1",
20 | "resolve-url-loader": "^3.1.2",
21 | "sass": "^1.32.11",
22 | "sass-loader": "^11.0.1",
23 | "vue": "^2.6.12",
24 | "vue-loader": "^15.9.5",
25 | "vue-template-compiler": "^2.6.12"
26 | },
27 | "dependencies": {
28 | "-": "0.0.1",
29 | "laravel-echo": "^1.10.0",
30 | "moment": "^2.29.1",
31 | "pusher-js": "4.3.0",
32 | "save": "^2.4.0",
33 | "vue-observe-visibility": "^1.0.0",
34 | "vue-select-image": "^1.9.0",
35 | "vue-toast-notification": "^0.6.2",
36 | "vue2-datepicker": "^3.9.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Notifications/PostNotification.php:
--------------------------------------------------------------------------------
1 | status = $post->status == 'succeeded' ? 'success' : 'error';
21 | $this->message = "Post #$post->id status changed to $post->status ";
22 | $this->link = route('posts.show', $post->id);
23 | }
24 |
25 | public function via($notifiable)
26 | {
27 | return ['database', 'broadcast'];
28 | }
29 |
30 | public function toDatabase($notifiable): array
31 | {
32 | Cache::forget('notifications.' . $notifiable->id);
33 | return ['status' => $this->status, 'type' => 'post', 'message' => $this->message, 'link' => $this->link];
34 | }
35 |
36 | public function toBroadcast($notifiable)
37 | {
38 | return (new BroadcastMessage([
39 | 'id' => $this->id,
40 | 'data' => [
41 | 'status' => $this->status, // for front-end
42 | 'type' => 'post',
43 | 'message' => $this->message,
44 | 'link' => $this->link,
45 | ],
46 | 'created_at' => 'just now',
47 | ]));
48 | }
49 |
50 | public function toArray($notifiable)
51 | {
52 | return [
53 | //
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/PostaBot/PublisherProviders/Facebook.php:
--------------------------------------------------------------------------------
1 | uid;
14 | $message = $post->message;
15 | $token = $account->token;
16 |
17 | if ($post->media->count() > 0) {
18 | $url = "https://graph.facebook.com/$uid/feed?message=$message&access_token=$token";
19 |
20 | foreach ($post->media as $index => $media) {
21 | $id = $this->uploadSinglePhoto($uid, $token, $media->original_path);
22 | $url .= "&attached_media%5B$index%5D=%7B%22media_fbid%22%3A%22$id%22%7D";
23 | }
24 | $response = Http::post($url);
25 |
26 | } else {
27 | $response = Http::post("https://graph.facebook.com/$uid/feed?message=$message&access_token=$token");
28 | }
29 | } catch (\Throwable $th) {
30 | throw new Exception("internal error happened");
31 | }
32 |
33 | if (!$response->successful()) {
34 | throw new Exception($response->json()['error']['message']);
35 | }
36 |
37 | }
38 |
39 | private function uploadSinglePhoto($uid, $token, $path)
40 | {
41 | $response = Http::attach('attachment', file_get_contents(storage_path("app/{$path}")), 'photo.jpg')->post("https://graph.facebook.com/$uid/photos", ['access_token' => $token, 'published' => false]);
42 | return $response->json()['id'];
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/app/Services/MediaService.php:
--------------------------------------------------------------------------------
1 | user()->id . "/original";
14 | $thumbPath = "media/" . auth()->user()->id . "/thumb";
15 |
16 | foreach ($request->file('media') as $file) {
17 | $mediaName = md5(Str::random(40) . microtime()) . "." . $file->getClientOriginalExtension();
18 | $mediaOriginalPath = $file->storeAs($originalPath, $mediaName); // save original at storage/app/media/{id}/original/{name}
19 | $mediaThumbPath = $file->storeAs($thumbPath, $mediaName); // save thumbnail at storage/app/media/{id}/thumb/{name}
20 | Image::make(storage_path("app/{$mediaThumbPath}"))->fit(120, 120)->save(); // resize image thumbnail
21 | $m = auth()->user()->media()->create([
22 | 'name' => $mediaName,
23 | 'original_path' => $mediaOriginalPath,
24 | 'thumb_path' => $mediaThumbPath,
25 | ]);
26 | $media[] = ['id' => $m->id, 'src' => route('media.show.thumb', $mediaName)];
27 | }
28 |
29 | return $media;
30 | }
31 |
32 | public static function delete($media)
33 | {
34 | if ($media->posts()->where(['draft' => 0, 'locked' => 0])->count() != 0) {
35 | return ['success' => false, 'message' => "Media #$media->id attached to some queued posts , please delete posts first"];
36 | }
37 | $media->delete();
38 | return ['success' => true];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/Http/Controllers/MediaController.php:
--------------------------------------------------------------------------------
1 | json(['success' => true, 'media' => $media]);
24 | }
25 |
26 | public function showOriginal($mediaName)
27 | {
28 | $userId = auth()->id();
29 | if (preg_match('/^[a-f0-9]{32}.(jpg|png|jpeg)$/i', $mediaName) && Storage::disk('local')->exists("media/{$userId}/original/{$mediaName}")) {
30 | return response()->file(storage_path("app/media/{$userId}/original/{$mediaName}"));
31 | }
32 | abort('404');
33 | }
34 |
35 | public function showThumb($mediaName)
36 | {
37 | $userId = auth()->id();
38 | if (preg_match('/^[a-f0-9]{32}.(jpg|png|jpeg)$/i', $mediaName) && Storage::disk('local')->exists("media/{$userId}/thumb/{$mediaName}")) {
39 | return response()->file(storage_path("app/media/{$userId}/thumb/{$mediaName}"));
40 | }
41 | abort('404');
42 | }
43 |
44 | public function destroy(Media $media)
45 | {
46 | $this->authorize('delete', $media);
47 | $response = MediaService::delete($media);
48 | return response()->json($response);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/Http/Controllers/ProfileController.php:
--------------------------------------------------------------------------------
1 | _avatar) {
25 | ProfileService::storeAvatar($request);
26 | }
27 | auth()->user()->update($request->validated());
28 | return redirect()->route('profile.index')->with('status', 'Your information updated successfully');
29 | }
30 |
31 | public function updatePassword(ProfilePasswordRequest $request)
32 | {
33 | auth()->user()->update(['password' => Hash::make($request->password)]);
34 | return redirect()->route('profile.index')->with('status', 'Your password updated successfully');
35 | }
36 |
37 | public function destroySession($id)
38 | {
39 | $session = auth()->user()->session()->where(['pub_id' => $id])->firstOrFail();
40 | $this->authorize('delete', $session);
41 | Session::getHandler()->destroy($session->id);
42 | return redirect()->route('profile.index')->with('status', 'Session destroyed successfully');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 'facebook' => [
34 | 'client_id' => env('FACEBOOK_CLIENT_ID'),
35 | 'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
36 | 'redirect' => env('FACEBOOK_REDIRECT'),
37 | ],
38 |
39 | 'google' => [
40 | 'client_id' => env('GOOGLE_CLIENT_ID'),
41 | 'client_secret' => env('GOOGLE_CLIENT_SECRET'),
42 | 'redirect' => env('GOOGLE_REDIRECT'),
43 | ],
44 |
45 | 'twitter' => [
46 | 'client_id' => env('TWITTER_CLIENT_ID'),
47 | 'client_secret' => env('TWITTER_CLIENT_SECRET'),
48 | 'redirect' => env('TWITTER_REDIRECT'),
49 | ],
50 | 'instagram' => [
51 | 'client_id' => env('INSTAGRAM_CLIENT_ID'),
52 | 'client_secret' => env('INSTAGRAM_CLIENT_SECRET'),
53 | 'redirect' => env('INSTAGRAM_REDIRECT'),
54 | ],
55 |
56 | ];
57 |
--------------------------------------------------------------------------------
/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);
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class);
50 |
51 | $response = tap($kernel->handle(
52 | $request = Request::capture()
53 | ))->send();
54 |
55 | $kernel->terminate($request, $response);
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Laravel LaPosta
5 |
6 |
7 | social media scheduler
8 |
9 |
10 |
11 | ## About The Project
12 | demo project built with laravel to schedule social media posts for later publish .. project focuses on using laravel framework components , architecture, patterns and tests.
13 |
14 |
15 | ## prerequisite
16 | try to make twitter or facebook application with publish permissions
17 | and update tokens in .env file
18 |
19 | ## Usage
20 |
21 | 1. Clone the repo
22 |
23 | ```sh
24 | git clone https://github.com/civilcoder55/laravel-laposta.git
25 | ```
26 |
27 | 2. update env file
28 |
29 | ```sh
30 | cp .env.example .env
31 | ```
32 |
33 | 3. start workers
34 |
35 | ```sh
36 | php artisan queue:work
37 | ```
38 |
39 | ```sh
40 | php artisan schedule:work
41 | ```
42 |
43 | ```sh
44 | php artisan websockets:serve
45 | ```
46 |
47 | 5. start server
48 |
49 | ```sh
50 | php artisan serve
51 | ```
52 |
53 | 6. access website at
54 |
55 | ```sh
56 | http://127.0.0.1:8000
57 | ```
58 |
59 | ## Screenshots
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | ## Video Preview
75 |
76 | - [Youtube Video](https://www.youtube.com/watch?v=38HfwgmgL-8)
77 |
78 | ## Built With
79 |
80 | - [Laravel](https://laravel.com)
81 |
82 |
83 |
--------------------------------------------------------------------------------
/resources/views/auth/passwords/confirm.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.auth')
2 | @section('title','Confirm Your Password')
3 | @section('content')
4 |
5 |
6 |
You forgot your password? Here you can easily retrieve a new password.
7 | @if (session('status'))
8 |
9 | {{ session('status') }}
10 |
11 | @endif
12 |
37 | @if (Route::has('password.request'))
38 |
39 | I forgot my password
40 |
41 | @endif
42 |
43 |
44 |
45 |
46 | @endsection
47 |
--------------------------------------------------------------------------------
/resources/views/main/posts/create.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.app')
2 | @section('title', 'Create Post')
3 | @section('content')
4 |
5 |
6 |
7 | @if (session('status'))
8 |
9 |
10 |
11 | {{ session('status') }}
12 |
13 |
14 |
15 | @endif
16 | @if ($errors->any())
17 |
18 |
19 |
20 |
21 | @foreach ($errors->all() as $error)
22 | {{ $error }}
23 | @endforeach
24 |
25 |
26 |
27 |
28 | @endif
29 |
46 |
47 |
48 |
49 | @endsection
--------------------------------------------------------------------------------
/app/Http/Requests/PostRequest.php:
--------------------------------------------------------------------------------
1 | ['required', 'boolean'],
22 | 'accounts' => ['required_if:draft,==,0', 'array', 'min:1', new PostAccountsCheck],
23 | 'accounts.*' => ['distinct', 'integer'],
24 | 'message' => ['required_if:draft,==,0', 'string', 'nullable'],
25 | 'media' => ['array', 'min:1', new PostMediaCheck],
26 | 'media.*' => ['distinct', 'integer'],
27 | 'schedule_date' => ['required_if:draft,==,0', 'date_format:d/m/Y h:i A', 'after:now', 'nullable'],
28 | ];
29 | }
30 |
31 | public function withValidator($validator)
32 | {
33 | if (!$validator->fails()) {
34 | $validator->after(function ($validator) {
35 | // convert date to timestamp
36 | if ($this->schedule_date) {
37 | $this->merge(['schedule_date' => Carbon::createFromFormat('d/m/Y h:i A', $this->schedule_date)->timestamp]);
38 | }
39 | // convert non media to empty array
40 | if (!$this->media) {
41 | $this->merge(['media' => []]);
42 | }
43 | // convert non accounts to empty array
44 | if (!$this->accounts) {
45 | $this->merge(['accounts' => []]);
46 | }
47 |
48 | });
49 | }
50 |
51 | }
52 |
53 | public function messages()
54 | {
55 | return [
56 | 'accounts.required_if' => 'Please select at least one account',
57 | 'message.required_if' => 'Please add text message',
58 | 'schedule_date.required_if' => 'Please select publish date and time',
59 | ];
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/resources/views/auth/passwords/email.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.auth')
2 | @section('title', 'Forgot Password')
3 | @section('content')
4 |
5 |
6 |
You forgot your password? Here you can easily retrieve a new password.
7 | @if (session('status'))
8 |
9 | {{ session('status') }}
10 |
11 | @endif
12 |
38 |
39 |
40 | Login
41 |
42 |
43 | Register a new membership
44 |
45 |
46 |
47 |
48 |
49 | @endsection
50 |
--------------------------------------------------------------------------------
/app/Services/PostService.php:
--------------------------------------------------------------------------------
1 | user()->posts()->create(['draft' => (bool) $request->draft, 'message' => $request->message, 'schedule_date' => $request->schedule_date, 'logs' => $log]);
14 | $post->media()->attach($request->media);
15 | $post->accounts()->attach($request->accounts);
16 | }
17 | public static function update($request, $post)
18 | {
19 | $post->draft = (bool) $request->draft ? 1 : 0;
20 | $post->message = $request->message;
21 | $post->schedule_date = $request->schedule_date;
22 | $mediaSync = $post->media()->sync($request->media);
23 | $accountsSync = $post->accounts()->sync($request->accounts);
24 | if ($post->isDirty() || ($mediaSync['attached'] || $mediaSync['detached']) || ($accountsSync['attached'] || $accountsSync['detached'])) {
25 | $log = static::updateLog($request);
26 | $post->logs = $log;
27 | $post->save();
28 | }
29 | }
30 | private static function storeLog($request)
31 | {
32 | $now = Carbon::now()->toDateTimeString();
33 | $then = Carbon::createFromTimestamp($request->schedule_date)->toDateTimeString();
34 | if ($request->draft) {
35 | $message = "Post saved as draft at {$now}";
36 | } else {
37 | $message = "Post saved and scheduled at {$now} to be published at {$then}";
38 | }
39 | return [['status' => 'info', 'message' => $message]];
40 | }
41 |
42 | private static function updateLog($request)
43 | {
44 | $now = Carbon::now()->toDateTimeString();
45 | $then = Carbon::createFromTimestamp($request->schedule_date)->toDateTimeString();
46 | if ($request->draft) {
47 | $message = "Post updated and saved as draft at {$now}";
48 | } else {
49 | $message = "Post updated and scheduled at {$now} to be published at {$then}";
50 | }
51 | return [['status' => 'info', 'message' => $message]];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/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 | 'host' => env('APP_URL', 'localhost'),
41 | 'port' => 6001,
42 | 'scheme' => env('PUSHER_APP_SCHEME', 'http'),
43 | 'encrypted' => true,
44 | //'useTLS' => true,
45 | 'curl_options' => [
46 | CURLOPT_SSL_VERIFYHOST => 0,
47 | CURLOPT_SSL_VERIFYPEER => 0,
48 | ],
49 |
50 | ],
51 | ],
52 |
53 | 'redis' => [
54 | 'driver' => 'redis',
55 | 'connection' => 'default',
56 | ],
57 |
58 | 'log' => [
59 | 'driver' => 'log',
60 | ],
61 |
62 | 'null' => [
63 | 'driver' => 'null',
64 | ],
65 |
66 | ],
67 |
68 | ];
69 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/RegisterController.php:
--------------------------------------------------------------------------------
1 | middleware('guest');
42 | }
43 |
44 | /**
45 | * Get a validator for an incoming registration request.
46 | *
47 | * @param array $data
48 | * @return \Illuminate\Contracts\Validation\Validator
49 | */
50 | protected function validator(array $data)
51 | {
52 | return Validator::make($data, [
53 | 'name' => ['required', 'string', 'max:255'],
54 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
55 | 'password' => ['required', 'string', 'min:8', 'confirmed'],
56 | ]);
57 | }
58 |
59 | /**
60 | * Create a new user instance after a valid registration.
61 | *
62 | * @param array $data
63 | * @return User
64 | */
65 | protected function create(array $data)
66 | {
67 | return User::create([
68 | 'name' => $data['name'],
69 | 'email' => $data['email'],
70 | 'password' => Hash::make($data['password']),
71 | ]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel/laravel",
3 | "type": "project",
4 | "description": "The Laravel Framework.",
5 | "keywords": [
6 | "framework",
7 | "laravel"
8 | ],
9 | "license": "MIT",
10 | "require": {
11 | "php": "^7.4.1",
12 | "abraham/twitteroauth": "^2.0",
13 | "beyondcode/laravel-websockets": "^1.11",
14 | "doctrine/dbal": "^2.12",
15 | "fideloper/proxy": "^4.2",
16 | "fruitcake/laravel-cors": "^2.0",
17 | "guzzlehttp/guzzle": "^7.2",
18 | "intervention/image": "^2.5",
19 | "laravel/framework": "^8.0",
20 | "laravel/socialite": "^5.0",
21 | "laravel/tinker": "^2.0",
22 | "laravel/ui": "^3.0",
23 | "predis/predis": "^1.1",
24 | "pusher/pusher-php-server": "~3.0",
25 | "whichbrowser/parser": "^2.1"
26 | },
27 | "require-dev": {
28 | "barryvdh/laravel-debugbar": "^3.5",
29 | "facade/ignition": "^2.3.6",
30 | "fzaninotto/faker": "^1.9.1",
31 | "mockery/mockery": "^1.3.1",
32 | "nunomaduro/collision": "^5.0",
33 | "phpunit/phpunit": "^9.3"
34 | },
35 | "config": {
36 | "optimize-autoloader": true,
37 | "preferred-install": "dist",
38 | "sort-packages": true
39 | },
40 | "extra": {
41 | "laravel": {
42 | "dont-discover": []
43 | }
44 | },
45 | "autoload": {
46 | "psr-4": {
47 | "App\\": "app/",
48 | "Database\\Factories\\": "database/factories/",
49 | "Database\\Seeders\\": "database/seeders/"
50 | }
51 | },
52 | "autoload-dev": {
53 | "psr-4": {
54 | "Tests\\": "tests/"
55 | }
56 | },
57 | "minimum-stability": "dev",
58 | "prefer-stable": true,
59 | "scripts": {
60 | "post-autoload-dump": [
61 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
62 | "@php artisan package:discover --ansi"
63 | ],
64 | "post-root-package-install": [
65 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
66 | ],
67 | "post-create-project-cmd": [
68 | "@php artisan key:generate --ansi"
69 | ]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/Http/Controllers/PostController.php:
--------------------------------------------------------------------------------
1 | query('type') == 'drafted') {
16 | $posts = UserRepository::getDraftedPosts();
17 | } else {
18 | $posts = UserRepository::getQueuedPosts();
19 | }
20 | return view('main.posts.index', compact(['posts']));
21 | }
22 |
23 | public function create()
24 | {
25 | $userMedia = UserRepository::getMedia(['id', 'name']);
26 | $userAccounts = UserRepository::getAccounts(['id', 'type', 'name']);
27 | return view('main.posts.create', compact(['userMedia', 'userAccounts']));
28 | }
29 |
30 | public function store(PostRequest $request)
31 | {
32 | PostService::store($request);
33 | return redirect()->route('posts.create')->with('status', 'your post saved successfuly');
34 | }
35 |
36 | public function show(Post $post)
37 | {
38 | $this->authorize('show', $post);
39 | $post->load(['media:id,name']);
40 | return view('main.posts.show', compact(['post']));
41 | }
42 | public function edit(Post $post)
43 | {
44 | $this->authorize('edit', $post);
45 | $post->load(['accounts:id,name,type', 'media:id,name']);
46 | $userAccounts = UserRepository::getAccounts(['name', 'type', 'id']);
47 | $userMedia = UserRepository::getMedia();
48 | return view('main.posts.edit', compact(['userAccounts', 'userMedia', 'post']));
49 | }
50 |
51 | public function update(PostRequest $request, Post $post)
52 | {
53 | $this->authorize('edit', $post);
54 | PostService::update($request, $post);
55 | return redirect()->route('posts.edit', $post->id)->with('status', 'your post updated successfully');
56 | }
57 |
58 | public function destroy(Post $post)
59 | {
60 | $this->authorize('delete', $post);
61 | $post->delete();
62 | return redirect()->route('posts.index', ($post->draft ? ['type' => 'drafted'] : ''))->with('status', 'your post deleted successfully');
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/Repositories/UserRepository.php:
--------------------------------------------------------------------------------
1 | user()->posts()->selectRaw("count(*) AS total,
12 | SUM(IF(draft = 1 , 1, 0)) AS drafts,
13 | SUM(IF(draft = 0 AND status='pending' , 1, 0)) AS queued,
14 | SUM(IF(draft = 0 AND status='succeeded', 1, 0)) AS succeeded,
15 | SUM(IF(draft = 1 AND status='failed', 1, 0)) AS failed"
16 | )->first();
17 | $accounts = auth()->user()->accounts()->count();
18 |
19 | return [
20 | 'posts' => $posts->total,
21 | 'drafts' => $posts->drafts ?? 0,
22 | 'queued' => $posts->queued ?? 0,
23 | 'succeeded' => $posts->succeeded ?? 0,
24 | 'failed' => $posts->failed ?? 0,
25 | 'accounts' => $accounts,
26 | ];
27 | }
28 |
29 | public static function getAccounts($fields = "*")
30 | {
31 | return auth()->user()->accounts()->orderBy('created_at', 'desc')->get($fields);
32 | }
33 |
34 | public static function getScheduledPosts()
35 | {
36 | return auth()->user()->posts()->where(['draft' => 0, 'locked' => 0])->get();
37 | }
38 |
39 | public static function getMedia($fields = "*")
40 | {
41 | return auth()->user()->media()->get($fields);
42 | }
43 |
44 | public static function getDraftedPosts()
45 | {
46 | return auth()->user()->posts()->where(['draft' => 1])->withCount(['accounts', 'media'])->orderBy('updated_at', 'desc')->paginate(16);
47 | }
48 |
49 | public static function getQueuedPosts()
50 | {
51 | return auth()->user()->posts()->where(['draft' => 0])->withCount(['accounts', 'media'])->orderBy('updated_at', 'desc')->paginate(16);
52 | }
53 |
54 | public static function getNotifications()
55 | {
56 | return auth()->user()->notifications()->orderBy('created_at', 'desc')->paginate(15);
57 | }
58 |
59 | public static function getSessions()
60 | {
61 | return auth()->user()->session()->orderBy('last_activity', 'desc')->get()->toArray();
62 | }
63 |
64 | public static function getSocialStatus()
65 | {
66 | $facebook = auth()->user()->socialAuthUser()->where(['provider' => 'facebook'])->first();
67 | $google = auth()->user()->socialAuthUser()->where(['provider' => 'google'])->first();
68 |
69 | return [
70 | "facebook" => $facebook,
71 | "google" => $google,
72 | ];
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/app/PostaBot/PublisherProviders/Twitter.php:
--------------------------------------------------------------------------------
1 | token;
16 | $secret = $account->secret;
17 |
18 | $this->connection = new TwitterOAuth($consumer_key, $consumer_secret, $token, $secret);
19 | $this->connection->setTimeouts(30, 60);
20 | // split messages every 280 chars as twitter max chars is 280
21 | $messages_array = str_split($post->message, 280);
22 |
23 | // upload media
24 | $media_ids = "";
25 | foreach ($post->media as $index => $media) {
26 | $id = $this->uploadSinglePhoto($media->original_path);
27 | $media_ids .= "$id,";
28 | }
29 |
30 | //split every 4 media as twitter max media per post is 4
31 | $media_ids_array = array_map(function ($arr) {
32 | return implode(",", $arr);
33 | }, array_chunk(explode(",", $media_ids), 4));
34 |
35 | $tweets = array_map(function ($message, $media_ids) {
36 | $result = [];
37 | if ($message) {$result['status'] = $message;}
38 | if ($media_ids) {$result['media_ids'] = $media_ids;}
39 | return $result;
40 | }, $messages_array, $media_ids_array);
41 |
42 | $parent = "";
43 | foreach ($tweets as $tweet) {
44 | if ($parent) {
45 | $tweet = array_merge($tweet, ['in_reply_to_status_id' => $parent]);
46 | }
47 |
48 | $result = $this->connection->post('statuses/update', $tweet);
49 |
50 | if ($this->connection->getLastHttpCode() != 200) {
51 | throw new \Exception($this->connection->getLastBody()->errors[0]->message);
52 | }
53 |
54 | $parent = $result->id;
55 | }
56 |
57 | } catch (\Exception $ex) {
58 | throw new \Exception($ex->getMessage());
59 | }
60 |
61 | }
62 |
63 | private function uploadSinglePhoto($path)
64 | {
65 | $media = $this->connection->upload('media/upload', ['media' => storage_path("app/{$path}")]);
66 | return $media->media_id_string;
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/app/Services/SocialAuthService.php:
--------------------------------------------------------------------------------
1 | id]])->first();
18 | $loggedIn = Auth::check();
19 | // login user with his linked social account
20 | if ($socialUser && !$loggedIn) {
21 | Auth::login($socialUser->user);
22 | return redirect()->route('dashboard');
23 | }
24 |
25 | // redirect if user already logged in
26 | if ($socialUser && $loggedIn) {
27 | return redirect()->route('profile.index');
28 | }
29 |
30 | // link existing user with social account
31 | if (!$socialUser && $loggedIn) {
32 | $alreadyExist = User::where(['email' => $data->email])->first();
33 | if ($alreadyExist) {
34 | return redirect()->route('profile.index')->with('status', 'already linked to another laposta account');
35 | }
36 | auth()->user()->socialAuthUser()->create(['provider' => $provider, 'uid' => $data->id, 'token' => $data->token]);
37 | return redirect()->route('profile.index');
38 | }
39 |
40 | //register with social account
41 |
42 | if (!$socialUser && !$loggedIn) {
43 | $alreadyExist = User::where(['email' => $data->email])->first();
44 | $user = $alreadyExist ? $alreadyExist : User::create(['email' => $data->email, 'name' => $data->name, 'password' => Hash::make(Str::random(24))]);
45 | $user->socialAuthUser()->create(['provider' => $provider, 'uid' => $data->id, 'token' => $data->token]);
46 | Auth::login($user);
47 | return redirect()->route('dashboard');
48 | }
49 |
50 | return redirect()->route('login')->with('status', 'Error happend ');
51 | }
52 |
53 | public static function revokeFacebookToken($token)
54 | {
55 | $response = Http::delete("https://graph.facebook.com/v2.4/me/permissions?access_token={$token}");
56 | }
57 |
58 | public static function revokeGoogleToken($token)
59 | {
60 | $response = Http::withHeaders([
61 | 'Content-type' => 'application/x-www-form-urlencoded',
62 | ])->post("https://oauth2.googleapis.com/revoke?token={$token}");
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/resources/views/main/posts/edit.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.app')
2 | @section('title', 'Edit Post')
3 | @section('content')
4 |
5 |
6 |
7 | @if (session('status'))
8 |
9 |
10 |
11 | {{ session('status') }}
12 |
13 |
14 |
15 | @endif
16 | @if ($errors->any())
17 |
18 |
19 |
20 |
21 | @foreach ($errors->all() as $error)
22 | {{ $error }}
23 | @endforeach
24 |
25 |
26 |
27 |
28 | @endif
29 |
30 |
37 |
38 |
44 |
45 |
51 |
52 | @foreach ($post->logs as $log)
53 |
54 |
{{ $log['status'] }}
55 |
{{ $log['message'] }}
56 |
57 | @endforeach
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | @endsection
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | where(['provider' => '(facebook|google)'])->name('connect.social');
8 | Route::get('/login/{provider}/callback', 'SocialAuthController@callback');
9 |
10 | Route::group(['middleware' => 'auth'], function () {
11 | Route::redirect('/', '/dashboard');
12 | Route::get('/disconnect/facebook', 'SocialAuthController@disconnectFacebook')->name('disconnect.facebook');
13 | Route::get('/disconnect/google', 'SocialAuthController@disconnectGoogle')->name('disconnect.google');
14 | Route::get('/dashboard', 'DashboardController@index')->name('dashboard');
15 | Route::get('/calendar', 'DashboardController@calendar')->name('calendar');
16 |
17 | Route::prefix('notifications')->group(function () {
18 | Route::get('/', 'NotificationController@index')->name('notifications.index');
19 | Route::put('/{notification}', 'NotificationController@read')->name('notifications.read');
20 | Route::delete('delete/all', 'NotificationController@destroyAll')->name('notifications.destroy.all');
21 | Route::delete('delete/{notification}', 'NotificationController@destroy')->name('notifications.destroy');
22 | });
23 |
24 | Route::prefix('profile')->group(function () {
25 | Route::get('/', 'ProfileController@index')->name('profile.index');
26 | Route::post('/', 'ProfileController@update')->name('profile.update');
27 | Route::post('password', 'ProfileController@updatePassword')->name('profile.update.password');
28 | Route::get('session/destroy/{id}', 'ProfileController@destroySession')->name('profile.session.destroy');
29 | });
30 |
31 | Route::prefix('media')->group(function () {
32 | Route::get('/', 'MediaController@index')->name('media.index');
33 | Route::post('/', 'MediaController@store')->name('media.store');
34 | Route::get('original/{mediaName}', 'MediaController@showOriginal')->name('media.show.original');
35 | Route::get('thumb/{mediaName}', 'MediaController@showThumb')->name('media.show.thumb');
36 | Route::delete('delete/{media}', 'MediaController@destroy')->name('media.destroy');
37 | });
38 |
39 | Route::prefix('accounts')->where(['platform' => '(facebook|twitter)'])->group(function () { //will add |instagram later ...
40 | Route::get('/', 'AccountController@index')->name('accounts.index');
41 | Route::get('{platform}/connect', 'AccountController@connect')->name('accounts.connect');
42 | Route::get('{platform}/callback', 'AccountController@callback')->name('accounts.connect.callback');
43 | Route::delete('delete/{account}', 'AccountController@destroy')->name('accounts.destroy');
44 | });
45 |
46 | Route::resource('posts', 'PostController');
47 |
48 | });
49 |
--------------------------------------------------------------------------------
/tests/Feature/AuthTest.php:
--------------------------------------------------------------------------------
1 | get('/register')->assertSuccessful()->assertViewIs('auth.register');
20 | }
21 |
22 | /** @test */
23 | public function guest_can_view_login_page()
24 | {
25 | $this->get('/login')->assertSuccessful()->assertViewIs('auth.login');
26 | }
27 |
28 | /** @test */
29 | public function user_cant_view_register_page()
30 | {
31 | $user = User::factory()->make();
32 | $this->actingAs($user)->get('/register')->assertRedirect('/dashboard');
33 | }
34 |
35 | /** @test */
36 | public function user_cant_view_login_page()
37 | {
38 | $user = User::factory()->make();
39 | $this->actingAs($user)->get('/login')->assertRedirect('/dashboard');
40 | }
41 |
42 | /** @test */
43 | public function user_can_register()
44 | {
45 | $this->post('/register', [
46 | 'name' => $this->faker->name,
47 | 'email' => $this->faker->email,
48 | 'password' => 'password',
49 | 'password_confirmation' => 'password',
50 | ])->assertRedirect('/dashboard');
51 | $this->assertDatabaseCount('users', 1);
52 | }
53 |
54 | /** @test */
55 | public function user_cant_register_with_same_email()
56 | {
57 | $user = User::factory()->create();
58 | $this->from('/register')->post('/register', [
59 | 'name' => $this->faker->name,
60 | 'email' => $user->email,
61 | 'password' => 'password',
62 | 'password_confirmation' => 'password',
63 | ])->assertRedirect('/register')->assertSessionHasErrors(['email']);
64 | $this->assertGuest()->assertDatabaseCount('users', 1);
65 | }
66 |
67 | /** @test */
68 | public function valid_user_can_login()
69 | {
70 | Notification::fake();
71 | $user = User::factory()->create();
72 | $this->post('/login', [
73 | 'email' => $user->email,
74 | 'password' => 'password',
75 | ])->assertRedirect('/dashboard');
76 | $this->assertAuthenticatedAs($user);
77 | }
78 |
79 | /** @test */
80 | public function invalid_user_cant_login()
81 | {
82 | $this->from('/login')->post('/login', [
83 | 'email' => 'wrong_email@gmail.com',
84 | 'password' => 'wrongPassword',
85 | ])->assertRedirect('/login')->assertSessionHasErrors(['email']);
86 | $this->assertGuest();
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/config/filesystems.php:
--------------------------------------------------------------------------------
1 | env('FILESYSTEM_DRIVER', 'local'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Default Cloud Filesystem Disk
21 | |--------------------------------------------------------------------------
22 | |
23 | | Many applications store files both locally and in the cloud. For this
24 | | reason, you may specify a default "cloud" driver here. This driver
25 | | will be bound as the Cloud disk implementation in the container.
26 | |
27 | */
28 |
29 | 'cloud' => env('FILESYSTEM_CLOUD', 's3'),
30 |
31 | /*
32 | |--------------------------------------------------------------------------
33 | | Filesystem Disks
34 | |--------------------------------------------------------------------------
35 | |
36 | | Here you may configure as many filesystem "disks" as you wish, and you
37 | | may even configure multiple disks of the same driver. Defaults have
38 | | been setup for each driver as an example of the required options.
39 | |
40 | | Supported Drivers: "local", "ftp", "sftp", "s3"
41 | |
42 | */
43 |
44 | 'disks' => [
45 |
46 | 'local' => [
47 | 'driver' => 'local',
48 | 'root' => storage_path('app'),
49 | ],
50 |
51 | 'public' => [
52 | 'driver' => 'local',
53 | 'root' => storage_path('app/public'),
54 | 'url' => env('APP_URL') . '/storage',
55 | 'visibility' => 'public',
56 | ],
57 |
58 | 's3' => [
59 | 'driver' => 's3',
60 | 'key' => env('AWS_ACCESS_KEY_ID'),
61 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
62 | 'region' => env('AWS_DEFAULT_REGION'),
63 | 'bucket' => env('AWS_BUCKET'),
64 | 'url' => env('AWS_URL'),
65 | 'endpoint' => env('AWS_ENDPOINT'),
66 | ],
67 |
68 | ],
69 |
70 | /*
71 | |--------------------------------------------------------------------------
72 | | Symbolic Links
73 | |--------------------------------------------------------------------------
74 | |
75 | | Here you may configure the symbolic links that will be created when the
76 | | `storage:link` Artisan command is executed. The array keys should be
77 | | the locations of the links and the values should be their targets.
78 | |
79 | */
80 |
81 | 'links' => [
82 | public_path('storage') => storage_path('app/public'),
83 | ],
84 |
85 | ];
86 |
--------------------------------------------------------------------------------
/config/queue.php:
--------------------------------------------------------------------------------
1 | env('QUEUE_CONNECTION', 'sync'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Queue Connections
21 | |--------------------------------------------------------------------------
22 | |
23 | | Here you may configure the connection information for each server that
24 | | is used by your application. A default configuration has been added
25 | | for each back-end shipped with Laravel. You are free to add more.
26 | |
27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'sync' => [
34 | 'driver' => 'sync',
35 | ],
36 |
37 | 'database' => [
38 | 'driver' => 'database',
39 | 'table' => 'jobs',
40 | 'queue' => 'default',
41 | 'retry_after' => 90,
42 | ],
43 |
44 | 'beanstalkd' => [
45 | 'driver' => 'beanstalkd',
46 | 'host' => 'localhost',
47 | 'queue' => 'default',
48 | 'retry_after' => 90,
49 | 'block_for' => 0,
50 | ],
51 |
52 | 'sqs' => [
53 | 'driver' => 'sqs',
54 | 'key' => env('AWS_ACCESS_KEY_ID'),
55 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
56 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
57 | 'queue' => env('SQS_QUEUE', 'your-queue-name'),
58 | 'suffix' => env('SQS_SUFFIX'),
59 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
60 | ],
61 |
62 | 'redis' => [
63 | 'driver' => 'redis',
64 | 'connection' => 'default',
65 | 'queue' => env('REDIS_QUEUE', 'default'),
66 | 'retry_after' => 90,
67 | 'block_for' => null,
68 | ],
69 |
70 | ],
71 |
72 | /*
73 | |--------------------------------------------------------------------------
74 | | Failed Queue Jobs
75 | |--------------------------------------------------------------------------
76 | |
77 | | These options configure the behavior of failed queue job logging so you
78 | | can control which database and table are used to store the jobs that
79 | | have failed. You may change them to any database / table you wish.
80 | |
81 | */
82 |
83 | 'failed' => [
84 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
85 | 'database' => env('DB_CONNECTION', 'mysql'),
86 | 'table' => 'failed_jobs',
87 | ],
88 |
89 | ];
90 |
--------------------------------------------------------------------------------
/app/Http/Kernel.php:
--------------------------------------------------------------------------------
1 | [
54 | EncryptCookies::class,
55 | AddQueuedCookiesToResponse::class,
56 | StartSession::class,
57 | // \Illuminate\Session\Middleware\AuthenticateSession::class,
58 | ShareErrorsFromSession::class,
59 | VerifyCsrfToken::class,
60 | SubstituteBindings::class,
61 | ],
62 |
63 | 'api' => [
64 | 'throttle:api',
65 | SubstituteBindings::class,
66 | ],
67 | ];
68 |
69 | /**
70 | * The application's route middleware.
71 | *
72 | * These middleware may be assigned to groups or used individually.
73 | *
74 | * @var array
75 | */
76 | protected $routeMiddleware = [
77 | 'auth' => Authenticate::class,
78 | 'auth.basic' => AuthenticateWithBasicAuth::class,
79 | 'cache.headers' => SetCacheHeaders::class,
80 | 'can' => Authorize::class,
81 | 'guest' => RedirectIfAuthenticated::class,
82 | 'password.confirm' => RequirePassword::class,
83 | 'signed' => ValidateSignature::class,
84 | 'throttle' => ThrottleRequests::class,
85 | 'verified' => EnsureEmailIsVerified::class,
86 | ];
87 | }
88 |
--------------------------------------------------------------------------------
/resources/views/auth/passwords/reset.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.auth')
2 | @section('title','Recover Password')
3 | @section('content')
4 |
5 |
6 |
7 |
You are only one step a way from your new password, recover your password now.
8 |
9 |
60 |
61 |
62 | Login
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | @endsection
71 |
--------------------------------------------------------------------------------
/resources/js/components/Notifications.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
108 |
--------------------------------------------------------------------------------
/resources/js/components/MediaUploader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
107 |
--------------------------------------------------------------------------------
/config/logging.php:
--------------------------------------------------------------------------------
1 | env('LOG_CHANNEL', 'stack'),
21 |
22 | /*
23 | |--------------------------------------------------------------------------
24 | | Log Channels
25 | |--------------------------------------------------------------------------
26 | |
27 | | Here you may configure the log channels for your application. Out of
28 | | the box, Laravel uses the Monolog PHP logging library. This gives
29 | | you a variety of powerful log handlers / formatters to utilize.
30 | |
31 | | Available Drivers: "single", "daily", "slack", "syslog",
32 | | "errorlog", "monolog",
33 | | "custom", "stack"
34 | |
35 | */
36 |
37 | 'channels' => [
38 | 'stack' => [
39 | 'driver' => 'stack',
40 | 'channels' => ['single'],
41 | 'ignore_exceptions' => false,
42 | ],
43 |
44 | 'single' => [
45 | 'driver' => 'single',
46 | 'path' => storage_path('logs/laravel.log'),
47 | 'level' => env('LOG_LEVEL', 'debug'),
48 | ],
49 |
50 | 'daily' => [
51 | 'driver' => 'daily',
52 | 'path' => storage_path('logs/laravel.log'),
53 | 'level' => env('LOG_LEVEL', 'debug'),
54 | 'days' => 14,
55 | ],
56 |
57 | 'slack' => [
58 | 'driver' => 'slack',
59 | 'url' => env('LOG_SLACK_WEBHOOK_URL'),
60 | 'username' => 'Laravel Log',
61 | 'emoji' => ':boom:',
62 | 'level' => env('LOG_LEVEL', 'critical'),
63 | ],
64 |
65 | 'papertrail' => [
66 | 'driver' => 'monolog',
67 | 'level' => env('LOG_LEVEL', 'debug'),
68 | 'handler' => SyslogUdpHandler::class,
69 | 'handler_with' => [
70 | 'host' => env('PAPERTRAIL_URL'),
71 | 'port' => env('PAPERTRAIL_PORT'),
72 | ],
73 | ],
74 |
75 | 'stderr' => [
76 | 'driver' => 'monolog',
77 | 'handler' => StreamHandler::class,
78 | 'formatter' => env('LOG_STDERR_FORMATTER'),
79 | 'with' => [
80 | 'stream' => 'php://stderr',
81 | ],
82 | ],
83 |
84 | 'syslog' => [
85 | 'driver' => 'syslog',
86 | 'level' => env('LOG_LEVEL', 'debug'),
87 | ],
88 |
89 | 'errorlog' => [
90 | 'driver' => 'errorlog',
91 | 'level' => env('LOG_LEVEL', 'debug'),
92 | ],
93 |
94 | 'null' => [
95 | 'driver' => 'monolog',
96 | 'handler' => NullHandler::class,
97 | ],
98 |
99 | 'emergency' => [
100 | 'path' => storage_path('logs/laravel.log'),
101 | ],
102 | ],
103 |
104 | ];
105 |
--------------------------------------------------------------------------------
/config/cache.php:
--------------------------------------------------------------------------------
1 | env('CACHE_DRIVER', 'file'),
22 |
23 | /*
24 | |--------------------------------------------------------------------------
25 | | Cache Stores
26 | |--------------------------------------------------------------------------
27 | |
28 | | Here you may define all of the cache "stores" for your application as
29 | | well as their drivers. You may even define multiple stores for the
30 | | same cache driver to group types of items stored in your caches.
31 | |
32 | */
33 |
34 | 'stores' => [
35 |
36 | 'apc' => [
37 | 'driver' => 'apc',
38 | ],
39 |
40 | 'array' => [
41 | 'driver' => 'array',
42 | 'serialize' => false,
43 | ],
44 |
45 | 'database' => [
46 | 'driver' => 'database',
47 | 'table' => 'cache',
48 | 'connection' => null,
49 | ],
50 |
51 | 'file' => [
52 | 'driver' => 'file',
53 | 'path' => storage_path('framework/cache/data'),
54 | ],
55 |
56 | 'memcached' => [
57 | 'driver' => 'memcached',
58 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
59 | 'sasl' => [
60 | env('MEMCACHED_USERNAME'),
61 | env('MEMCACHED_PASSWORD'),
62 | ],
63 | 'options' => [
64 | // Memcached::OPT_CONNECT_TIMEOUT => 2000,
65 | ],
66 | 'servers' => [
67 | [
68 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
69 | 'port' => env('MEMCACHED_PORT', 11211),
70 | 'weight' => 100,
71 | ],
72 | ],
73 | ],
74 |
75 | 'redis' => [
76 | 'driver' => 'redis',
77 | 'connection' => 'cache',
78 | ],
79 |
80 | 'dynamodb' => [
81 | 'driver' => 'dynamodb',
82 | 'key' => env('AWS_ACCESS_KEY_ID'),
83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
86 | 'endpoint' => env('DYNAMODB_ENDPOINT'),
87 | ],
88 |
89 | ],
90 |
91 | /*
92 | |--------------------------------------------------------------------------
93 | | Cache Key Prefix
94 | |--------------------------------------------------------------------------
95 | |
96 | | When utilizing a RAM based store such as APC or Memcached, there might
97 | | be other applications utilizing the same cache. So, we'll specify a
98 | | value to get prefixed to all our keys so we can avoid collisions.
99 | |
100 | */
101 |
102 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'),
103 |
104 | ];
105 |
--------------------------------------------------------------------------------
/config/mail.php:
--------------------------------------------------------------------------------
1 | env('MAIL_MAILER', 'smtp'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Mailer Configurations
21 | |--------------------------------------------------------------------------
22 | |
23 | | Here you may configure all of the mailers used by your application plus
24 | | their respective settings. Several examples have been configured for
25 | | you and you are free to add your own as your application requires.
26 | |
27 | | Laravel supports a variety of mail "transport" drivers to be used while
28 | | sending an e-mail. You will specify which one you are using for your
29 | | mailers below. You are free to add additional mailers as required.
30 | |
31 | | Supported: "smtp", "sendmail", "mailgun", "ses",
32 | | "postmark", "log", "array"
33 | |
34 | */
35 |
36 | 'mailers' => [
37 | 'smtp' => [
38 | 'transport' => 'smtp',
39 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
40 | 'port' => env('MAIL_PORT', 587),
41 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'),
42 | 'username' => env('MAIL_USERNAME'),
43 | 'password' => env('MAIL_PASSWORD'),
44 | 'timeout' => null,
45 | 'auth_mode' => null,
46 | ],
47 |
48 | 'ses' => [
49 | 'transport' => 'ses',
50 | ],
51 |
52 | 'mailgun' => [
53 | 'transport' => 'mailgun',
54 | ],
55 |
56 | 'postmark' => [
57 | 'transport' => 'postmark',
58 | ],
59 |
60 | 'sendmail' => [
61 | 'transport' => 'sendmail',
62 | 'path' => '/usr/sbin/sendmail -bs',
63 | ],
64 |
65 | 'log' => [
66 | 'transport' => 'log',
67 | 'channel' => env('MAIL_LOG_CHANNEL'),
68 | ],
69 |
70 | 'array' => [
71 | 'transport' => 'array',
72 | ],
73 | ],
74 |
75 | /*
76 | |--------------------------------------------------------------------------
77 | | Global "From" Address
78 | |--------------------------------------------------------------------------
79 | |
80 | | You may wish for all e-mails sent by your application to be sent from
81 | | the same address. Here, you may specify a name and address that is
82 | | used globally for all e-mails that are sent by your application.
83 | |
84 | */
85 |
86 | 'from' => [
87 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
88 | 'name' => env('MAIL_FROM_NAME', 'Example'),
89 | ],
90 |
91 | /*
92 | |--------------------------------------------------------------------------
93 | | Markdown Mail Settings
94 | |--------------------------------------------------------------------------
95 | |
96 | | If you are using Markdown based email rendering, you may configure your
97 | | theme and component paths here, allowing you to customize the design
98 | | of the emails. Or, you may simply stick with the Laravel defaults!
99 | |
100 | */
101 |
102 | 'markdown' => [
103 | 'theme' => 'default',
104 |
105 | 'paths' => [
106 | resource_path('views/vendor/mail'),
107 | ],
108 | ],
109 |
110 | ];
111 |
--------------------------------------------------------------------------------
/resources/views/main/dashboard.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.app')
2 | @section('title', 'Dashboard')
3 | @section('content')
4 |
5 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Accounts
22 | {{ $statistics['accounts'] }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Posts
31 | {{ $statistics['posts']}}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Drafts
40 | {{ $statistics['drafts']}}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Queued
49 | {{ $statistics['queued']}}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | succeeded
60 | {{ $statistics['succeeded'] }}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Failed
69 | {{ $statistics['failed']}}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | @endsection
--------------------------------------------------------------------------------
/resources/views/auth/login.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.auth')
2 | @section('title', 'Login')
3 |
4 | @section('content')
5 |
6 |
7 |
Sign in to start your session
8 | @if (session('status'))
9 |
10 | {{ session('status') }}
11 |
12 | @endif
13 |
62 |
63 |
73 |
74 |
75 |
76 | @if (Route::has('password.request'))
77 | I forgot my password
78 | @endif
79 |
80 |
81 |
82 | Register a new membership
83 |
84 |
85 |
86 |
87 |
88 | @endsection
89 |
--------------------------------------------------------------------------------
/config/auth.php:
--------------------------------------------------------------------------------
1 | [
17 | 'guard' => 'web',
18 | 'passwords' => 'users',
19 | ],
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | Authentication Guards
24 | |--------------------------------------------------------------------------
25 | |
26 | | Next, you may define every authentication guard for your application.
27 | | Of course, a great default configuration has been defined for you
28 | | here which uses session storage and the Eloquent user provider.
29 | |
30 | | All authentication drivers have a user provider. This defines how the
31 | | users are actually retrieved out of your database or other storage
32 | | mechanisms used by this application to persist your user's data.
33 | |
34 | | Supported: "session", "token"
35 | |
36 | */
37 |
38 | 'guards' => [
39 | 'web' => [
40 | 'driver' => 'session',
41 | 'provider' => 'users',
42 | ],
43 |
44 | 'api' => [
45 | 'driver' => 'token',
46 | 'provider' => 'users',
47 | 'hash' => false,
48 | ],
49 | ],
50 |
51 | /*
52 | |--------------------------------------------------------------------------
53 | | User Providers
54 | |--------------------------------------------------------------------------
55 | |
56 | | All authentication drivers have a user provider. This defines how the
57 | | users are actually retrieved out of your database or other storage
58 | | mechanisms used by this application to persist your user's data.
59 | |
60 | | If you have multiple user tables or models you may configure multiple
61 | | sources which represent each model / table. These sources may then
62 | | be assigned to any extra authentication guards you have defined.
63 | |
64 | | Supported: "database", "eloquent"
65 | |
66 | */
67 |
68 | 'providers' => [
69 | 'users' => [
70 | 'driver' => 'eloquent',
71 | 'model' => App\Models\User::class,
72 | ],
73 |
74 | // 'users' => [
75 | // 'driver' => 'database',
76 | // 'table' => 'users',
77 | // ],
78 | ],
79 |
80 | /*
81 | |--------------------------------------------------------------------------
82 | | Resetting Passwords
83 | |--------------------------------------------------------------------------
84 | |
85 | | You may specify multiple password reset configurations if you have more
86 | | than one user table or model in the application and you want to have
87 | | separate password reset settings based on the specific user types.
88 | |
89 | | The expire time is the number of minutes that the reset token should be
90 | | considered valid. This security feature keeps tokens short-lived so
91 | | they have less time to be guessed. You may change this as needed.
92 | |
93 | */
94 |
95 | 'passwords' => [
96 | 'users' => [
97 | 'provider' => 'users',
98 | 'table' => 'password_resets',
99 | 'expire' => 60,
100 | 'throttle' => 60,
101 | ],
102 | ],
103 |
104 | /*
105 | |--------------------------------------------------------------------------
106 | | Password Confirmation Timeout
107 | |--------------------------------------------------------------------------
108 | |
109 | | Here you may define the amount of seconds before a password confirmation
110 | | times out and the user is prompted to re-enter their password via the
111 | | confirmation screen. By default, the timeout lasts for three hours.
112 | |
113 | */
114 |
115 | 'password_timeout' => 10800,
116 |
117 | ];
118 |
--------------------------------------------------------------------------------
/resources/views/main/posts/index.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.app')
2 | @section('title', 'Posts')
3 | @section('stylesheet')
4 |
22 | @endsection
23 | @section('content')
24 |
25 |
26 |
27 | @if (session('status'))
28 |
29 |
30 |
31 | {{ session('status') }}
32 |
33 |
34 |
35 | @endif
36 |
37 |
38 |
39 |
42 |
43 |
44 |
45 |
46 | #
47 | Message
48 | Media
49 | Accounts
50 | Status
51 | Created_At
52 | To_Published_At
53 | Action
54 |
55 |
56 |
57 | @foreach ($posts as $index => $post)
58 |
59 | {{ $post->id }}
60 |
61 | {{ $post->message }}
62 | {{ $post->media_count }}
63 | {{ $post->accounts_count}}
64 | {{ $post->draft ? 'draft' : $post->status }}
65 | {{ $post->created_at }}
66 | {{ $post->schedule_date }}
67 |
68 |
79 |
80 |
81 | @endforeach
82 |
83 |
84 |
85 | @if ($posts->hasPages())
86 |
89 | @endif
90 |
91 |
92 |
93 |
94 |
95 |
96 | @endsection
--------------------------------------------------------------------------------
/resources/views/auth/register.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.auth')
2 | @section('title', 'Registers')
3 | @section('content')
4 |
92 |
93 |
94 |
95 |
96 |
97 | @endsection
98 |
--------------------------------------------------------------------------------
/resources/js/components/PostCreator.vue:
--------------------------------------------------------------------------------
1 |
2 |
104 |
105 |
106 |
144 |
--------------------------------------------------------------------------------
/resources/views/main/notifications.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.app')
2 | @section('title', 'Notifications')
3 | @section('content')
4 |
5 |
6 |
7 | @if (session('status'))
8 |
9 |
10 |
11 | {{ session('status') }}
12 |
13 |
14 |
15 | @endif
16 | @if (session('error'))
17 |
18 |
19 |
20 | {{ session('error') }}
21 |
22 |
23 |
24 | @endif
25 |
26 |
27 |
28 |
41 |
42 |
43 |
44 |
45 | Type
46 | Message
47 | Time
48 | Action
49 |
50 |
51 |
52 | @foreach ($allNotifications as $notification)
53 | @if ($notification->data['type'] == 'login')
54 |
55 | Login
56 |
57 | {{ $notification->data['message'] }}
58 | @elseif($notification->data["type"] == 'post')
59 |
60 | Post
61 | {{ $notification->data['message'] }}
62 | @endif
63 | {{ $notification->created_at}}
64 |
65 |
66 |
68 | @csrf
69 | @method('DELETE')
70 |
73 |
74 |
75 |
76 | @endforeach
77 |
78 |
79 |
80 | @if ($allNotifications->hasPages())
81 |
84 | @endif
85 |
86 |
87 |
88 |
89 |
90 |
91 | @endsection
--------------------------------------------------------------------------------