├── public ├── favicon.ico ├── robots.txt ├── .htaccess ├── web.config └── index.php ├── resources ├── sass │ └── app.scss ├── js │ ├── app.js │ └── bootstrap.js ├── views │ ├── emails │ │ └── invitations │ │ │ ├── invite-existing-user.blade.php │ │ │ └── invite-new-user.blade.php │ └── welcome.blade.php └── lang │ └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── debugbar │ └── .gitignore ├── app │ ├── .gitignore │ └── public │ │ ├── .gitignore │ │ └── uploads │ │ ├── .gitignore │ │ └── designs │ │ ├── large │ │ └── .gitignore │ │ ├── original │ │ └── .gitignore │ │ ├── thumbnail │ │ └── .gitignore │ │ └── .gitignore ├── framework │ ├── testing │ │ └── .gitignore │ ├── views │ │ └── .gitignore │ ├── cache │ │ ├── data │ │ │ └── .gitignore │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ └── .gitignore └── uploads │ ├── large │ └── .gitignore │ ├── original │ └── .gitignore │ ├── thumbnail │ └── .gitignore │ └── .gitignore ├── database ├── .gitignore ├── seeds │ └── DatabaseSeeder.php ├── migrations │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2020_01_23_003838_add_fields_to_designs.php │ ├── 2020_02_01_011027_add_team_id_to_designs.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2020_01_31_224355_create_messages_table.php │ ├── 2020_01_23_021358_create_jobs_table.php │ ├── 2020_01_26_031336_create_comments_table.php │ ├── 2020_01_27_232327_create_likes_table.php │ ├── 2020_01_22_213725_create_designs_table.php │ ├── 2020_01_29_234339_create_invitations_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2020_01_31_224345_create_chats_table.php │ ├── 2020_01_29_214651_create_teams_table.php │ └── 2020_01_25_114136_create_taggable_table.php └── factories │ └── UserFactory.php ├── config ├── site.php ├── services.php ├── view.php ├── cors.php ├── hashing.php ├── broadcasting.php ├── filesystems.php ├── taggable.php ├── queue.php ├── logging.php ├── cache.php ├── auth.php ├── mail.php ├── database.php └── session.php ├── .gitattributes ├── app ├── Repositories │ ├── Contracts │ │ ├── IComment.php │ │ ├── IMessage.php │ │ ├── ITeam.php │ │ ├── IChat.php │ │ ├── IUser.php │ │ ├── IInvitation.php │ │ ├── IDesign.php │ │ └── IBase.php │ ├── Criteria │ │ ├── ICriterion.php │ │ └── ICriteria.php │ └── Eloquent │ │ ├── Criteria │ │ ├── LatestFirst.php │ │ ├── IsLive.php │ │ ├── WithTrashed.php │ │ ├── ForUser.php │ │ └── EagerLoad.php │ │ ├── MessageRepository.php │ │ ├── CommentRepository.php │ │ ├── TeamRepository.php │ │ ├── InvitationRepository.php │ │ ├── ChatRepository.php │ │ ├── UserRepository.php │ │ ├── BaseRepository.php │ │ └── DesignRepository.php ├── Exceptions │ ├── ModelNotDefined.php │ └── Handler.php ├── Models │ ├── Like.php │ ├── Comment.php │ ├── Invitation.php │ ├── Message.php │ ├── Chat.php │ ├── Traits │ │ └── Likeable.php │ ├── Team.php │ ├── Design.php │ └── User.php ├── Http │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── CheckForMaintenanceMode.php │ │ ├── TrimStrings.php │ │ ├── TrustProxies.php │ │ ├── Authenticate.php │ │ ├── VerifyCsrfToken.php │ │ ├── RedirectIfAuthenticated.php │ │ └── ProfileJsonResponse.php │ ├── Controllers │ │ ├── Controller.php │ │ ├── User │ │ │ ├── MeController.php │ │ │ ├── UserController.php │ │ │ └── SettingsController.php │ │ ├── Auth │ │ │ ├── ForgotPasswordController.php │ │ │ ├── ResetPasswordController.php │ │ │ ├── ConfirmPasswordController.php │ │ │ ├── LoginController.php │ │ │ ├── RegisterController.php │ │ │ └── VerificationController.php │ │ ├── Designs │ │ │ ├── CommentController.php │ │ │ ├── UploadController.php │ │ │ └── DesignController.php │ │ ├── Chats │ │ │ └── ChatController.php │ │ └── Teams │ │ │ ├── TeamsController.php │ │ │ └── InvitationsController.php │ ├── Resources │ │ ├── MessageResource.php │ │ ├── TeamResource.php │ │ ├── ChatResource.php │ │ ├── CommentResource.php │ │ ├── UserResource.php │ │ └── DesignResource.php │ └── Kernel.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AppServiceProvider.php │ ├── EventServiceProvider.php │ ├── AuthServiceProvider.php │ ├── RepositoryServiceProvider.php │ └── RouteServiceProvider.php ├── Notifications │ ├── VerifyEmail.php │ └── ResetPassword.php ├── Rules │ ├── MatchOldPassword.php │ └── CheckSamePassword.php ├── Console │ └── Kernel.php ├── Mail │ └── SendInvitationToJoinTeam.php ├── Policies │ ├── TeamPolicy.php │ ├── MessagePolicy.php │ ├── DesignPolicy.php │ ├── CommentPolicy.php │ └── InvitationPolicy.php └── Jobs │ └── UploadImage.php ├── tests ├── TestCase.php ├── Unit │ └── ExampleTest.php ├── Feature │ └── ExampleTest.php └── CreatesApplication.php ├── .gitignore ├── .styleci.yml ├── .editorconfig ├── routes ├── channels.php ├── console.php ├── web.php └── api.php ├── webpack.mix.js ├── server.php ├── .env.example ├── package.json ├── phpunit.xml ├── artisan ├── composer.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | require('./bootstrap'); 2 | -------------------------------------------------------------------------------- /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/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/uploads/large/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/uploads/original/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/uploads/thumbnail/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !uploads/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/uploads/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !designs/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/uploads/designs/large/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/uploads/designs/original/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/uploads/designs/thumbnail/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/uploads/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !large/ 3 | !original/ 4 | !thumbnail/ 5 | !.gitignore 6 | -------------------------------------------------------------------------------- /config/site.php: -------------------------------------------------------------------------------- 1 | 'public' // 'public', 's3' 5 | ]; -------------------------------------------------------------------------------- /storage/app/public/uploads/designs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !large/ 3 | !original/ 4 | !thumbnail/ 5 | !.gitignore 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | CHANGELOG.md export-ignore 6 | -------------------------------------------------------------------------------- /app/Repositories/Contracts/IComment.php: -------------------------------------------------------------------------------- 1 | latest(); 12 | } 13 | } -------------------------------------------------------------------------------- /app/Repositories/Eloquent/Criteria/IsLive.php: -------------------------------------------------------------------------------- 1 | where('is_live', true); 12 | } 13 | } -------------------------------------------------------------------------------- /app/Repositories/Eloquent/Criteria/WithTrashed.php: -------------------------------------------------------------------------------- 1 | withTrashed(); 12 | } 13 | } -------------------------------------------------------------------------------- /app/Models/Like.php: -------------------------------------------------------------------------------- 1 | morphTo(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Repositories/Eloquent/MessageRepository.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Repositories/Eloquent/CommentRepository.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Repositories/Eloquent/Criteria/ForUser.php: -------------------------------------------------------------------------------- 1 | user_id = $user_id; 14 | } 15 | 16 | public function apply($model) 17 | { 18 | return $model->where('user_id', $this->user_id); 19 | } 20 | } -------------------------------------------------------------------------------- /app/Repositories/Contracts/IBase.php: -------------------------------------------------------------------------------- 1 | user()->teams; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/Repositories/Eloquent/Criteria/EagerLoad.php: -------------------------------------------------------------------------------- 1 | relationships = $relationships; 14 | } 15 | 16 | public function apply($model) 17 | { 18 | return $model->with($this->relationships); 19 | } 20 | } -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | team->name }}**. 6 | Because you are already registered to the platform, you just 7 | need to accept or reject the invitation in your 8 | [team management console]({{ $url }}). 9 | 10 | @component('mail::button', ['url' => $url]) 11 | Go to Dashboard 12 | @endcomponent 13 | 14 | Thanks,
15 | {{ config('app.name') }} 16 | @endcomponent 17 | -------------------------------------------------------------------------------- /app/Http/Controllers/User/MeController.php: -------------------------------------------------------------------------------- 1 | check()){ 16 | $user = auth()->user(); 17 | return new UserResource($user); 18 | } 19 | return response()->json(null, 401); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Models/Comment.php: -------------------------------------------------------------------------------- 1 | morphTo(); 19 | } 20 | 21 | public function user() 22 | { 23 | return $this->belongsTo(User::class); 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /resources/views/emails/invitations/invite-new-user.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # Hi, 3 | 4 | You have been invited to join the team 5 | **{{ $invitation->team->name }}**. 6 | Because you are not yet signed up to the platform, please 7 | [Register for free]({{ $url }}), then you can accept or reject the 8 | invitation in your team management console. 9 | 10 | @component('mail::button', ['url' => $url]) 11 | Register for free 12 | @endcomponent 13 | 14 | Thanks,
15 | {{ config('app.name') }} 16 | @endcomponent 17 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 16 | }); 17 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 18 | })->describe('Display an inspiring quote'); 19 | -------------------------------------------------------------------------------- /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/Repositories/Eloquent/InvitationRepository.php: -------------------------------------------------------------------------------- 1 | members()->attach($user_id); 18 | } 19 | 20 | public function removeUserFromTeam($team, $user_id) 21 | { 22 | $team->members()->detach($user_id); 23 | } 24 | 25 | 26 | } -------------------------------------------------------------------------------- /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 | # Handle Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /app/Models/Invitation.php: -------------------------------------------------------------------------------- 1 | belongsTo(Team::class); 20 | } 21 | 22 | public function recipient() 23 | { 24 | return $this->hasOne(User::class, 'email', 'recipient_email'); 25 | } 26 | 27 | public function sender() 28 | { 29 | return $this->hasOne(User::class, 'id', 'sender_id'); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Repositories/Eloquent/ChatRepository.php: -------------------------------------------------------------------------------- 1 | model->find($chatId); 17 | $chat->participants()->sync($data); 18 | } 19 | 20 | public function getUserChats() 21 | { 22 | return auth()->user()->chats() 23 | ->with(['messages', 'participants']) 24 | ->get(); 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ForgotPasswordController.php: -------------------------------------------------------------------------------- 1 | json(['status' => trans($response)], 200); 17 | } 18 | 19 | protected function sendResetlinkFailedResponse(Request $request, $response) 20 | { 21 | return response()->json(['email' => trans($response)], 422); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 22 | return redirect(RouteServiceProvider::HOME); 23 | } 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ResetPasswordController.php: -------------------------------------------------------------------------------- 1 | json(['status' => trans($response)], 200); 18 | } 19 | 20 | protected function sendResetFailedResponse(Request $request, $response) 21 | { 22 | return response()->json(['email' => trans($response)], 200); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | newQuery(); 8 | // if ($request->q) { 9 | // $query->orWhere(function($q) use ($request){ 10 | // $q->where('title', 'LIKE', '%'.$request->q.'%') 11 | // ->orWhere('description', 'LIKE', '%'.$request->q.'%'); 12 | // }); 13 | // } 14 | // $query->has('comments'); 15 | // $q->has('team'); 16 | // $q->withCount('likes') 17 | // ->orderByDesc('likes_count'); 18 | 19 | 20 | // get by tag 21 | //$q->withAllTags('adobe-photoshop'); 22 | // dd($query->get()); 23 | 24 | 25 | 26 | 27 | return view('welcome'); 28 | }); 29 | -------------------------------------------------------------------------------- /app/Notifications/VerifyEmail.php: -------------------------------------------------------------------------------- 1 | addMinutes(60), 22 | ['user' => $notifiable->id] 23 | ); 24 | 25 | return str_replace(url('/api'), $appUrl, $url); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/MessageResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'body' => $this->body, 20 | 'deleted' => $this->trashed(), 21 | 'dates' => [ 22 | 'created_at_human' => $this->created_at->diffForHumans(), 23 | 'created_at' => $this->created_at, 24 | ], 25 | 'sender' => new UserResource($this->sender) 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/TeamResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | 'total_members' => $this->members->count(), 21 | 'slug' => $this->slug, 22 | 'designs' => DesignResource::collection($this->designs), 23 | 'owner' => new UserResource($this->owner), 24 | 'member' => UserResource::collection($this->members) 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have e-mailed 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 e-mail address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /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 | [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | parent::boot(); 31 | 32 | // 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Resources/ChatResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'dates' => [ 20 | 'created_at_human' => $this->created_at->diffForHumans(), 21 | 'created_at' => $this->created_at, 22 | ], 23 | 'is_unread' => $this->isUnreadForUser(auth()->id()), 24 | 'latest_message' => new MessageResource($this->latest_message), 25 | 'participants' => UserResource::collection($this->participants) 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Notifications/ResetPassword.php: -------------------------------------------------------------------------------- 1 | token). 17 | '?email='.urlencode($notifiable->email); 18 | return (new MailMessage) 19 | ->line('you are receiving this email because we received a password reset request for your account') 20 | ->action('Reset Password', $url) 21 | ->line('If you did not request a password reset, no further action is required.'); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/2020_01_23_003838_add_fields_to_designs.php: -------------------------------------------------------------------------------- 1 | boolean('upload_successful')->default(false); 18 | $table->string('disk')->default('public'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('designs', function (Blueprint $table) { 30 | $table->dropColumn(['disk', 'upload_successful']); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2020_02_01_011027_add_team_id_to_designs.php: -------------------------------------------------------------------------------- 1 | bigInteger('team_id') 18 | ->after('user_id') 19 | ->unsigned() 20 | ->nullable(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::table('designs', function (Blueprint $table) { 32 | $table->dropColumn(['team_id']); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Rules/MatchOldPassword.php: -------------------------------------------------------------------------------- 1 | user()->password); 30 | } 31 | 32 | /** 33 | * Get the validation error message. 34 | * 35 | * @return string 36 | */ 37 | public function message() 38 | { 39 | return 'You have provided a wrong current password'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Rules/CheckSamePassword.php: -------------------------------------------------------------------------------- 1 | user()->password); 30 | } 31 | 32 | /** 33 | * Get the validation error message. 34 | * 35 | * @return string 36 | */ 37 | public function message() 38 | { 39 | return 'Your new password must be different from your current password'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Resources/CommentResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'body' => $this->body, 20 | 'created_at_dates' => [ 21 | 'created_at_human' => $this->created_at->diffForHumans(), 22 | 'created_at' => $this->created_at 23 | ], 24 | 'updated_at_dates' => [ 25 | 'updated_at_human' => $this->updated_at->diffForHumans(), 26 | 'updated_at' => $this->updated_at 27 | ], 28 | 'user' => new UserResource($this->user) 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->text('connection'); 19 | $table->text('queue'); 20 | $table->longText('payload'); 21 | $table->longText('exception'); 22 | $table->timestamp('failed_at')->useCurrent(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('failed_jobs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | window._ = require('lodash'); 2 | 3 | /** 4 | * We'll load the axios HTTP library which allows us to easily issue requests 5 | * to our Laravel back-end. This library automatically handles sending the 6 | * CSRF token as a header based on the value of the "XSRF" token cookie. 7 | */ 8 | 9 | window.axios = require('axios'); 10 | 11 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 12 | 13 | /** 14 | * Echo exposes an expressive API for subscribing to channels and listening 15 | * for events that are broadcast by Laravel. Echo and event broadcasting 16 | * allows your team to easily build robust real-time web applications. 17 | */ 18 | 19 | // import Echo from 'laravel-echo'; 20 | 21 | // window.Pusher = require('pusher-js'); 22 | 23 | // window.Echo = new Echo({ 24 | // broadcaster: 'pusher', 25 | // key: process.env.MIX_PUSHER_APP_KEY, 26 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 27 | // forceTLS: true 28 | // }); 29 | -------------------------------------------------------------------------------- /app/Models/Message.php: -------------------------------------------------------------------------------- 1 | trashed()){ 21 | if(!auth()->check()) return null; 22 | 23 | return auth()->id() == $this->sender->id ? 24 | 'You deleted this message' : 25 | "{$this->sender->name} deleted this message"; 26 | } 27 | return $value; 28 | } 29 | 30 | public function chat() 31 | { 32 | return $this->belongsTo(Chat::class); 33 | } 34 | 35 | public function sender() 36 | { 37 | return $this->belongsTo(User::class, 'user_id'); 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Faker $faker) { 20 | return [ 21 | 'name' => $faker->name, 22 | 'email' => $faker->unique()->safeEmail, 23 | 'email_verified_at' => now(), 24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 25 | 'remember_token' => Str::random(10), 26 | ]; 27 | }); 28 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=DesignHouse 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL= 6 | CLIENT_URL= 7 | 8 | LOG_CHANNEL=stack 9 | 10 | # MySql v5.7 11 | DB_CONNECTION=mysql 12 | DB_HOST=127.0.0.1 13 | DB_PORT=3306 14 | DB_DATABASE= 15 | DB_USERNAME= 16 | DB_PASSWORD= 17 | 18 | BROADCAST_DRIVER=log 19 | CACHE_DRIVER=file 20 | QUEUE_CONNECTION=sync 21 | SESSION_DRIVER=cookie 22 | SESSION_LIFETIME=120 23 | 24 | REDIS_HOST=127.0.0.1 25 | REDIS_PASSWORD=null 26 | REDIS_PORT=6379 27 | 28 | MAIL_DRIVER=smtp 29 | MAIL_HOST=smtp.mailtrap.io 30 | MAIL_PORT=2525 31 | MAIL_USERNAME= 32 | MAIL_PASSWORD= 33 | MAIL_ENCRYPTION=null 34 | 35 | AWS_ACCESS_KEY_ID= 36 | AWS_SECRET_ACCESS_KEY= 37 | AWS_DEFAULT_REGION=us-east-1 38 | AWS_BUCKET= 39 | 40 | PUSHER_APP_ID= 41 | PUSHER_APP_KEY= 42 | PUSHER_APP_SECRET= 43 | PUSHER_APP_CLUSTER=mt1 44 | 45 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 46 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 47 | 48 | JWT_SECRET=3JVhu6aTifZERvah1wNtH9aIpgqGBESbKmlcu6fg95LWE6BlSKnTeSGmffwoTHTX 49 | -------------------------------------------------------------------------------- /database/migrations/2020_01_31_224355_create_messages_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->bigInteger('user_id')->unsigned(); 19 | $table->bigInteger('chat_id')->unsigned(); 20 | $table->text('body'); 21 | $table->datetime('last_read')->nullable(); 22 | $table->softDeletes(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('messages'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire') 28 | // ->hourly(); 29 | } 30 | 31 | /** 32 | * Register the commands for the application. 33 | * 34 | * @return void 35 | */ 36 | protected function commands() 37 | { 38 | $this->load(__DIR__.'/Commands'); 39 | 40 | require base_path('routes/console.php'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/2020_01_23_021358_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/2020_01_26_031336_create_comments_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->bigInteger('user_id')->unsigned()->index(); 19 | $table->text('body'); 20 | $table->morphs('commentable'); 21 | $table->timestamps(); 22 | 23 | $table->foreign('user_id')->references('id') 24 | ->on('users')->onDelete('cascade'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('comments'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2020_01_27_232327_create_likes_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->bigInteger('user_id') 19 | ->unsigned()->index(); 20 | $table->morphs('likeable'); 21 | $table->timestamps(); 22 | 23 | $table->foreign('user_id') 24 | ->references('id') 25 | ->on('users') 26 | ->onDelete('cascade'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('likes'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 --hide-modules --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 --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 --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "axios": "^0.19", 14 | "cross-env": "^5.1", 15 | "laravel-mix": "^4.0.7", 16 | "lodash": "^4.17.13", 17 | "resolve-url-loader": "^2.3.1", 18 | "sass": "^1.15.2", 19 | "sass-loader": "^7.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | ]; 34 | -------------------------------------------------------------------------------- /app/Models/Chat.php: -------------------------------------------------------------------------------- 1 | belongsToMany(User::class, 'participants'); 14 | } 15 | 16 | public function messages() 17 | { 18 | return $this->hasMany(Message::class); 19 | } 20 | 21 | // helper 22 | public function getLatestMessageAttribute() 23 | { 24 | return $this->messages()->latest()->first(); 25 | } 26 | 27 | public function isUnreadForUser($userId) 28 | { 29 | return (bool)$this->messages() 30 | ->whereNull('last_read') 31 | ->where('user_id', '<>', $userId) 32 | ->count(); 33 | } 34 | 35 | public function markAsReadForUser($userId) 36 | { 37 | $this->messages() 38 | ->whereNull('last_read') 39 | ->where('user_id', '<>', $userId) 40 | ->update([ 41 | 'last_read' => Carbon::now() 42 | ]); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/Http/Controllers/User/UserController.php: -------------------------------------------------------------------------------- 1 | users = $users; 20 | } 21 | 22 | public function index() 23 | { 24 | $users = $this->users->withCriteria([ 25 | new EagerLoad(['designs']) 26 | ])->all(); 27 | 28 | return UserResource::collection($users); 29 | } 30 | 31 | public function search(Request $request) 32 | { 33 | $designers = $this->users->search($request); 34 | return UserResource::collection($designers); 35 | } 36 | 37 | public function findByUsername($username) 38 | { 39 | $user = $this->users->findWhereFirst('username', $username); 40 | return new UserResource($user); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/2020_01_22_213725_create_designs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->bigInteger('user_id')->unsigned(); 19 | $table->string('image'); 20 | $table->string('title')->nullable(); 21 | $table->string('description')->nullable(); 22 | $table->string('slug')->nullable(); 23 | $table->boolean('is_live')->default(false); 24 | $table->timestamps(); 25 | 26 | $table->foreign('user_id')->references('id')->on('users'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('designs'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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/Middleware/ProfileJsonResponse.php: -------------------------------------------------------------------------------- 1 | bound('debugbar') || ! app('debugbar')->isEnabled() ){ 25 | return $response; 26 | } 27 | 28 | // profile the json response 29 | if($response instanceof JsonResponse && $request->has('_debug')){ 30 | 31 | // $response->setData(array_merge($response->getData(true), [ 32 | // '_debugbar' => app('debugbar')->getData() 33 | // ])); 34 | $response->setData(array_merge([ 35 | '_debugbar' => Arr::only(app('debugbar')->getData(), 'queries') 36 | ], $response->getData(true))); 37 | 38 | } 39 | 40 | return $response; 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 27 | Design::class => DesignPolicy::class, 28 | Comment::class => CommentPolicy::class, 29 | Team::class => TeamPolicy::class, 30 | Invitation::class => InvitationPolicy::class, 31 | Message::class => MessagePolicy::class 32 | ]; 33 | 34 | /** 35 | * Register any authentication / authorization services. 36 | * 37 | * @return void 38 | */ 39 | public function boot() 40 | { 41 | $this->registerPolicies(); 42 | 43 | // 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Http/Resources/UserResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'username' => $this->username, 20 | $this->mergeWhen(auth()->check() && auth()->id() == $this->id, [ 21 | 'email' => $this->email, 22 | ]), 23 | 'name' => $this->name, 24 | 'photo_url' => $this->photo_url, 25 | 'designs' => DesignResource::collection( 26 | $this->whenLoaded('designs') 27 | ), 28 | 'create_dates' => [ 29 | 'created_at_human' => $this->created_at->diffForHumans(), 30 | 'created_at' => $this->created_at 31 | ], 32 | 'formatted_address' => $this->formatted_address, 33 | 'tagline' => $this->tagline, 34 | 'about' => $this->about, 35 | 'location' => $this->location, 36 | 'available_to_hire' => $this->available_to_hire, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2020_01_29_234339_create_invitations_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->bigInteger('team_id')->unsigned(); 19 | $table->bigInteger('sender_id')->unsigned(); 20 | $table->string('recipient_email')->index(); 21 | $table->string('token'); 22 | $table->timestamps(); 23 | 24 | $table->foreign('team_id') 25 | ->references('id') 26 | ->on('teams') 27 | ->onDelete('cascade'); 28 | 29 | $table->foreign('sender_id') 30 | ->references('id') 31 | ->on('users') 32 | ->onDelete('cascade'); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function down() 42 | { 43 | Schema::dropIfExists('invitations'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('username')->unique(); 19 | $table->string('name'); 20 | $table->string('email')->unique(); 21 | $table->timestamp('email_verified_at')->nullable(); 22 | $table->string('tagline')->nullable(); 23 | $table->text('about')->nullable(); 24 | $table->point('location')->nullable(); 25 | $table->string('formatted_address')->nullable(); 26 | $table->boolean('available_to_hire')->default(false); 27 | $table->string('password'); 28 | $table->rememberToken(); 29 | $table->timestamps(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists('users'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/2020_01_31_224345_create_chats_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->timestamps(); 19 | }); 20 | 21 | Schema::create('participants', function (Blueprint $table) { 22 | $table->bigInteger('chat_id')->unsigned(); 23 | $table->bigInteger('user_id')->unsigned(); 24 | 25 | $table->foreign('user_id') 26 | ->references('id') 27 | ->on('users') 28 | ->onDelete('cascade'); 29 | 30 | $table->foreign('chat_id') 31 | ->references('id') 32 | ->on('chats') 33 | ->onDelete('cascade'); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | * 40 | * @return void 41 | */ 42 | public function down() 43 | { 44 | Schema::dropIfExists('participants'); 45 | Schema::dropIfExists('chats'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Providers/RepositoryServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(IDesign::class, DesignRepository::class); 45 | $this->app->bind(IUser::class, UserRepository::class); 46 | $this->app->bind(IComment::class, CommentRepository::class); 47 | $this->app->bind(ITeam::class, TeamRepository::class); 48 | $this->app->bind(IInvitation::class, InvitationRepository::class); 49 | $this->app->bind(IChat::class, ChatRepository::class); 50 | $this->app->bind(IMessage::class, MessageRepository::class); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Models/Traits/Likeable.php: -------------------------------------------------------------------------------- 1 | removeLikes(); 12 | }); 13 | } 14 | 15 | // delete likes when model is being deleted 16 | public function removeLikes() 17 | { 18 | if($this->likes()->count()){ 19 | $this->likes()->delete(); 20 | } 21 | } 22 | 23 | 24 | public function likes() 25 | { 26 | return $this->morphMany(Like::class, 'likeable'); 27 | } 28 | 29 | public function like() 30 | { 31 | if(! auth()->check()) return; 32 | 33 | // check if the current user has already liked the model 34 | if($this->isLikedByUser(auth()->id())){ 35 | return; 36 | }; 37 | 38 | $this->likes()->create(['user_id' => auth()->id()]); 39 | } 40 | 41 | public function unlike() 42 | { 43 | if(! auth()->check()) return; 44 | 45 | if(! $this->isLikedByUser(auth()->id())){ 46 | return; 47 | } 48 | 49 | $this->likes() 50 | ->where('user_id', auth() 51 | ->id())->delete(); 52 | } 53 | 54 | public function isLikedByUser($user_id) 55 | { 56 | return (bool)$this->likes() 57 | ->where('user_id', $user_id) 58 | ->count(); 59 | } 60 | 61 | 62 | 63 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/Unit 16 | 17 | 18 | 19 | ./tests/Feature 20 | 21 | 22 | 23 | 24 | ./app 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/Models/Team.php: -------------------------------------------------------------------------------- 1 | user()->teams()->attach($team->id); 23 | $team->members()->attach(auth()->id()); 24 | }); 25 | 26 | static::deleting(function($team){ 27 | $team->members()->sync([]); 28 | }); 29 | 30 | } 31 | 32 | public function owner() 33 | { 34 | return $this->belongsTo(User::class, 'owner_id'); 35 | } 36 | 37 | public function members() 38 | { 39 | return $this->belongsToMany(User::class) 40 | ->withTimestamps(); 41 | } 42 | 43 | public function designs() 44 | { 45 | return $this->hasMany(Design::class); 46 | } 47 | 48 | public function hasUser(User $user) 49 | { 50 | return $this->members() 51 | ->where('user_id', $user->id) 52 | ->first() ? true : false; 53 | } 54 | 55 | public function invitations() 56 | { 57 | return $this->hasMany(Invitation::class); 58 | } 59 | 60 | public function hasPendingInvite($email) 61 | { 62 | return (bool)$this->invitations() 63 | ->where('recipient_email', $email) 64 | ->count(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /database/migrations/2020_01_29_214651_create_teams_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->string('slug')->unique()->index(); 20 | $table->bigInteger('owner_id')->unsigned(); 21 | $table->timestamps(); 22 | 23 | $table->foreign('owner_id') 24 | ->references('id') 25 | ->on('users'); 26 | 27 | }); 28 | 29 | Schema::create('team_user', function (Blueprint $table) { 30 | $table->bigInteger('team_id')->unsigned(); 31 | $table->bigInteger('user_id')->unsigned(); 32 | $table->timestamps(); 33 | 34 | $table->foreign('team_id') 35 | ->references('id') 36 | ->on('teams') 37 | ->onDelete('cascade'); 38 | 39 | $table->foreign('user_id') 40 | ->references('id') 41 | ->on('users') 42 | ->onDelete('cascade'); 43 | }); 44 | } 45 | 46 | /** 47 | * Reverse the migrations. 48 | * 49 | * @return void 50 | */ 51 | public function down() 52 | { 53 | Schema::dropIfExists('team_users'); 54 | Schema::dropIfExists('teams'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Models/Design.php: -------------------------------------------------------------------------------- 1 | 'boolean', 29 | 'upload_successful' => 'boolean', 30 | 'close_to_comments' => 'boolean' 31 | ]; 32 | 33 | public function user() 34 | { 35 | return $this->belongsTo(User::class); 36 | } 37 | 38 | public function team() 39 | { 40 | return $this->belongsTo(Team::class); 41 | } 42 | 43 | public function comments() 44 | { 45 | return $this->morphMany(Comment::class, 'commentable') 46 | ->orderBy('created_at', 'asc'); 47 | } 48 | 49 | public function getImagesAttribute() 50 | { 51 | 52 | return [ 53 | 'thumbnail' => $this->getImagePath('thumbnail'), 54 | 'large' => $this->getImagePath('large'), 55 | 'original' => $this->getImagePath('original'), 56 | ]; 57 | } 58 | 59 | protected function getImagePath($size) 60 | { 61 | return Storage::disk($this->disk) 62 | ->url("uploads/designs/{$size}/".$this->image); 63 | } 64 | 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/Http/Controllers/Designs/CommentController.php: -------------------------------------------------------------------------------- 1 | comments = $comments; 20 | $this->designs = $designs; 21 | } 22 | 23 | public function store(Request $request, $designId) 24 | { 25 | $this->validate($request, [ 26 | 'body' => ['required'] 27 | ]); 28 | 29 | $comment = $this->designs->addComment($designId, [ 30 | 'body' => $request->body, 31 | 'user_id' => auth()->id() 32 | ]); 33 | 34 | return new CommentResource($comment); 35 | } 36 | 37 | public function update(Request $request, $id) 38 | { 39 | $comment = $this->comments->find($id); 40 | $this->authorize('update', $comment); 41 | 42 | $this->validate($request, [ 43 | 'body' => ['required'] 44 | ]); 45 | $comment = $this->comments->update($id, [ 46 | 'body' => $request->body 47 | ]); 48 | return new CommentResource($comment); 49 | } 50 | 51 | public function destroy($id) 52 | { 53 | $comment = $this->comments->find($id); 54 | $this->authorize('delete', $comment); 55 | $this->comments->delete($id); 56 | return response()->json(['message' => 'Item deleted'], 200); 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*'], 25 | 26 | /* 27 | * Matches the request method. `[*]` allows all methods. 28 | */ 29 | 'allowed_methods' => ['*'], 30 | 31 | /* 32 | * Matches the request origin. `[*]` allows all origins. 33 | */ 34 | 'allowed_origins' => ['*'], 35 | 36 | /* 37 | * Matches the request origin with, similar to `Request::is()` 38 | */ 39 | 'allowed_origins_patterns' => [], 40 | 41 | /* 42 | * Sets the Access-Control-Allow-Headers response header. `[*]` allows all headers. 43 | */ 44 | 'allowed_headers' => ['*'], 45 | 46 | /* 47 | * Sets the Access-Control-Expose-Headers response header. 48 | */ 49 | 'exposed_headers' => false, 50 | 51 | /* 52 | * Sets the Access-Control-Max-Age response header. 53 | */ 54 | 'max_age' => false, 55 | 56 | /* 57 | * Sets the Access-Control-Allow-Credentials header. 58 | */ 59 | 'supports_credentials' => false, 60 | ]; 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Http/Resources/DesignResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'title' => $this->title, 20 | 'slug' => $this->slug, 21 | 'images' => $this->images, 22 | 'is_live' => $this->is_live, 23 | 'likes_count' => $this->likes()->count(), 24 | 'description' => $this->description, 25 | 'tag_list' => [ 26 | 'tags' => $this->tagArray, 27 | 'normalized' => $this->tagArrayNormalized, 28 | ], 29 | 'created_at_dates' => [ 30 | 'created_at_human' => $this->created_at->diffForHumans(), 31 | 'created_at' => $this->created_at 32 | ], 33 | 'updated_at_dates' => [ 34 | 'updated_at_human' => $this->updated_at->diffForHumans(), 35 | 'updated_at' => $this->updated_at 36 | ], 37 | 'team' => $this->team ? [ 38 | 'id' => $this->team->id, 39 | 'name' => $this->team->name, 40 | 'slug' => $this->team->slug 41 | ] : null, 42 | 'comments_count' => $this->comments()->count(), 43 | 'comments' => CommentResource::collection( 44 | $this->whenLoaded('comments')), 45 | 'user' => new UserResource($this->whenLoaded('user')) 46 | 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Mail/SendInvitationToJoinTeam.php: -------------------------------------------------------------------------------- 1 | invitation = $invitation; 26 | $this->user_exists = $user_exists; 27 | } 28 | 29 | /** 30 | * Build the message. 31 | * 32 | * @return $this 33 | */ 34 | public function build() 35 | { 36 | if($this->user_exists){ 37 | $url = config('app.client_url').'/settings/teams'; 38 | return $this->markdown('emails.invitations.invite-existing-user') 39 | ->subject('Invitation to join team '. $this->invitation->team->name) 40 | ->with([ 41 | 'invitation' => $this->invitation, 42 | 'url' => $url 43 | ]); 44 | } else { 45 | $url = config('app.client_url').'/register?invitation='.$this->invitation->recipient_email; 46 | return $this->markdown('emails.invitations.invite-new-user') 47 | ->subject('Invitation to join team '. $this->invitation->team->name) 48 | ->with([ 49 | 'invitation' => $this->invitation, 50 | 'url' => $url 51 | ]); 52 | } 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Repositories/Eloquent/UserRepository.php: -------------------------------------------------------------------------------- 1 | model 19 | ->where('email', $email) 20 | ->first(); 21 | } 22 | 23 | public function search(Request $request) 24 | { 25 | $query = (new $this->model)->newQuery(); 26 | 27 | // only designers who have designs 28 | if($request->has_designs){ 29 | $query->has('designs'); 30 | } 31 | 32 | // check for available_to_hire 33 | if($request->available_to_hire){ 34 | $query->where('available_to_hire', true); 35 | } 36 | 37 | // Geographic Search 38 | $lat = $request->latitude; 39 | $lng = $request->longitude; 40 | $dist = $request->distance; 41 | $unit = $request->unit; 42 | 43 | if($lat && $lng){ 44 | $point = new Point($lat, $lng); 45 | $unit == 'km' ? $dist *= 1000 : $dist *=1609.34; 46 | $query->distanceSphereExcludingSelf('location', $point, $dist); 47 | } 48 | 49 | // order the results 50 | if($request->orderBy=='closest'){ 51 | $query->orderByDistanceSphere('location', $point, 'asc'); 52 | } else if($request->orderBy=='latest'){ 53 | $query->latest(); 54 | } else { 55 | $query->oldest(); 56 | } 57 | 58 | return $query->get(); 59 | 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 'useTLS' => true, 41 | ], 42 | ], 43 | 44 | 'redis' => [ 45 | 'driver' => 'redis', 46 | 'connection' => 'default', 47 | ], 48 | 49 | 'log' => [ 50 | 'driver' => 'log', 51 | ], 52 | 53 | 'null' => [ 54 | 'driver' => 'null', 55 | ], 56 | 57 | ], 58 | 59 | ]; 60 | -------------------------------------------------------------------------------- /app/Http/Controllers/Designs/UploadController.php: -------------------------------------------------------------------------------- 1 | designs = $designs; 17 | } 18 | 19 | public function upload(Request $request) 20 | { 21 | // validate the request 22 | $this->validate($request, [ 23 | 'image' => ['required', 'mimes:jpeg,gif,bmp,png', 'max:2048'] 24 | ]); 25 | 26 | // get the image 27 | $image = $request->file('image'); 28 | $image_path = $image->getPathName(); 29 | 30 | 31 | // get the original file name and replace any spaces with _ 32 | // Business Cards.png = timestamp()_business_cards.png 33 | $filename = time()."_". preg_replace('/\s+/', '_', strtolower($image->getClientOriginalName())); 34 | 35 | // move the image to the temporary location (tmp) 36 | $tmp = $image->storeAs('uploads/original', $filename, 'tmp'); 37 | 38 | // create the database record for the design 39 | // $design = auth()->user()->designs()->create([ 40 | // 'image' => $filename, 41 | // 'disk' => config('site.upload_disk') 42 | // ]); 43 | 44 | $design = $this->designs->create([ 45 | 'user_id' => auth()->id(), 46 | 'image' => $filename, 47 | 'disk' => config('site.upload_disk') 48 | ]); 49 | 50 | // dispatch a job to handle the image manipulation 51 | $this->dispatch(new UploadImage($design)); 52 | 53 | return response()->json($design, 200); 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapApiRoutes(); 46 | 47 | $this->mapWebRoutes(); 48 | 49 | // 50 | } 51 | 52 | /** 53 | * Define the "web" routes for the application. 54 | * 55 | * These routes all receive session state, CSRF protection, etc. 56 | * 57 | * @return void 58 | */ 59 | protected function mapWebRoutes() 60 | { 61 | Route::middleware('web') 62 | ->namespace($this->namespace) 63 | ->group(base_path('routes/web.php')); 64 | } 65 | 66 | /** 67 | * Define the "api" routes for the application. 68 | * 69 | * These routes are typically stateless. 70 | * 71 | * @return void 72 | */ 73 | protected function mapApiRoutes() 74 | { 75 | Route::prefix('api') 76 | ->middleware('api') 77 | ->namespace($this->namespace) 78 | ->group(base_path('routes/api.php')); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | define('LARAVEL_START', microtime(true)); 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Register The Auto Loader 15 | |-------------------------------------------------------------------------- 16 | | 17 | | Composer provides a convenient, automatically generated class loader for 18 | | our application. We just need to utilize it! We'll simply require it 19 | | into the script here so that we don't have to worry about manual 20 | | loading any of our classes later on. It feels great to relax. 21 | | 22 | */ 23 | 24 | require __DIR__.'/../vendor/autoload.php'; 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Turn On The Lights 29 | |-------------------------------------------------------------------------- 30 | | 31 | | We need to illuminate PHP development, so let us turn on the lights. 32 | | This bootstraps the framework and gets it ready for use, then it 33 | | will load up this application so that we can run it and send 34 | | the responses back to the browser and delight our users. 35 | | 36 | */ 37 | 38 | $app = require_once __DIR__.'/../bootstrap/app.php'; 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Run The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once we have the application, we can handle the incoming request 46 | | through the kernel, and send the associated response back to 47 | | the client's browser allowing them to enjoy the creative 48 | | and wonderful application we have prepared for them. 49 | | 50 | */ 51 | 52 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 53 | 54 | $response = $kernel->handle( 55 | $request = Illuminate\Http\Request::capture() 56 | ); 57 | 58 | $response->send(); 59 | 60 | $kernel->terminate($request, $response); 61 | -------------------------------------------------------------------------------- /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.2", 12 | "cviebrock/eloquent-taggable": "^6.0", 13 | "fideloper/proxy": "^4.0", 14 | "fruitcake/laravel-cors": "^1.0", 15 | "grimzy/laravel-mysql-spatial": "^2.2", 16 | "intervention/image": "^2.5", 17 | "laravel/framework": "^6.2", 18 | "laravel/tinker": "^2.0", 19 | "league/flysystem-aws-s3-v3": "^1.0", 20 | "tymon/jwt-auth": "^1.0.0-rc.5" 21 | }, 22 | "require-dev": { 23 | "barryvdh/laravel-debugbar": "^3.2", 24 | "facade/ignition": "^1.4", 25 | "fzaninotto/faker": "^1.4", 26 | "mockery/mockery": "^1.0", 27 | "nunomaduro/collision": "^3.0", 28 | "phpunit/phpunit": "^8.0" 29 | }, 30 | "config": { 31 | "optimize-autoloader": true, 32 | "preferred-install": "dist", 33 | "sort-packages": true 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "dont-discover": [] 38 | } 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "App\\": "app/" 43 | }, 44 | "classmap": [ 45 | "database/seeds", 46 | "database/factories" 47 | ] 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Tests\\": "tests/" 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true, 56 | "scripts": { 57 | "post-autoload-dump": [ 58 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 59 | "@php artisan package:discover --ansi" 60 | ], 61 | "post-root-package-install": [ 62 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 63 | ], 64 | "post-create-project-cmd": [ 65 | "@php artisan key:generate --ansi" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/Http/Controllers/User/SettingsController.php: -------------------------------------------------------------------------------- 1 | users = $users; 19 | } 20 | 21 | public function updateProfile(Request $request) 22 | { 23 | $user = auth()->user(); 24 | 25 | $this->validate($request, [ 26 | 'tagline' => ['required'], 27 | 'name' => ['required'], 28 | 'about' => ['required', 'string', 'min:20'], 29 | 'formatted_address' => ['required'], 30 | 'location.latitude' => ['required', 'numeric', 'min:-90', 'max:90'], 31 | 'location.longitude' => ['required', 'numeric', 'min:-180', 'max:180'] 32 | ]); 33 | 34 | $location = new Point($request->location['latitude'], $request->location['longitude']); 35 | 36 | 37 | $user = $this->users->update(auth()->id(), [ 38 | 'name' => $request->name, 39 | 'formatted_address' => $request->formatted_address, 40 | 'location' => $location, 41 | 'available_to_hire' => $request->available_to_hire, 42 | 'about' => $request->about, 43 | 'tagline' => $request->tagline, 44 | ]); 45 | 46 | return new UserResource($user); 47 | 48 | } 49 | 50 | public function updatePassword(Request $request) 51 | { 52 | 53 | $this->validate($request, [ 54 | 'current_password' => ['required', new MatchOldPassword], 55 | 'password' => ['required', 'confirmed', 'min:6', new CheckSamePassword], 56 | ]); 57 | 58 | $this->users->update(auth()->id(), [ 59 | 'password' => bcrypt($request->password) 60 | ]); 61 | 62 | return response()->json(['message' => 'Password updated'], 200); 63 | 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/Repositories/Eloquent/BaseRepository.php: -------------------------------------------------------------------------------- 1 | model = $this->getModelClass(); 19 | } 20 | 21 | public function all() 22 | { 23 | return $this->model->get(); 24 | } 25 | 26 | public function find($id) 27 | { 28 | $result = $this->model->findOrFail($id); 29 | return $result; 30 | } 31 | 32 | public function findWhere($column, $value) 33 | { 34 | return $this->model->where($column, $value)->get(); 35 | } 36 | 37 | 38 | public function findWhereFirst($column, $value) 39 | { 40 | return $this->model->where($column, $value)->firstOrFail(); 41 | } 42 | 43 | public function paginate($perPage = 10) 44 | { 45 | return $this->model->paginate($perPage); 46 | } 47 | 48 | public function create(array $data) 49 | { 50 | $result = $this->model->create($data); 51 | return $result; 52 | } 53 | 54 | public function update($id, array $data) 55 | { 56 | $record = $this->find($id); 57 | $record->update($data); 58 | return $record; 59 | } 60 | 61 | public function delete($id) 62 | { 63 | $record = $this->find($id); 64 | return $record->delete(); 65 | } 66 | 67 | 68 | public function withCriteria(...$criteria) 69 | { 70 | $criteria = Arr::flatten($criteria); 71 | 72 | foreach($criteria as $criterion){ 73 | $this->model = $criterion->apply($this->model); 74 | } 75 | 76 | return $this; 77 | } 78 | 79 | 80 | 81 | protected function getModelClass() 82 | { 83 | if( !method_exists($this, 'model')) 84 | { 85 | throw new ModelNotDefined(); 86 | } 87 | 88 | return app()->make($this->model()); 89 | 90 | } 91 | 92 | 93 | 94 | 95 | 96 | } -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | expectsJson()){ 54 | return response()->json(["errors" => [ 55 | "message" => "You are not authorized to access this resource" 56 | ]], 403); 57 | } 58 | } 59 | 60 | if($exception instanceof ModelNotFoundException && $request->expectsJson()){ 61 | return response()->json(["errors" => [ 62 | "message" => "The resource was not found in the database" 63 | ]], 404); 64 | } 65 | 66 | if($exception instanceof ModelNotDefined && $request->expectsJson()){ 67 | return response()->json(["errors" => [ 68 | "message" => "No model defined" 69 | ]], 500); 70 | } 71 | 72 | 73 | 74 | 75 | return parent::render($request, $exception); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/Policies/TeamPolicy.php: -------------------------------------------------------------------------------- 1 | isOwnerOfTeam($team); 57 | } 58 | 59 | /** 60 | * Determine whether the user can delete the team. 61 | * 62 | * @param \App\Models\User $user 63 | * @param \App\Models\Team $team 64 | * @return mixed 65 | */ 66 | public function delete(User $user, Team $team) 67 | { 68 | return $user->isOwnerOfTeam($team); 69 | } 70 | 71 | /** 72 | * Determine whether the user can restore the team. 73 | * 74 | * @param \App\Models\User $user 75 | * @param \App\Models\Team $team 76 | * @return mixed 77 | */ 78 | public function restore(User $user, Team $team) 79 | { 80 | // 81 | } 82 | 83 | /** 84 | * Determine whether the user can permanently delete the team. 85 | * 86 | * @param \App\Models\User $user 87 | * @param \App\Models\Team $team 88 | * @return mixed 89 | */ 90 | public function forceDelete(User $user, Team $team) 91 | { 92 | // 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Policies/MessagePolicy.php: -------------------------------------------------------------------------------- 1 | id == $message->user_id; 69 | } 70 | 71 | /** 72 | * Determine whether the user can restore the message. 73 | * 74 | * @param \App\Models\User $user 75 | * @param \App\Models\Message $message 76 | * @return mixed 77 | */ 78 | public function restore(User $user, Message $message) 79 | { 80 | // 81 | } 82 | 83 | /** 84 | * Determine whether the user can permanently delete the message. 85 | * 86 | * @param \App\Models\User $user 87 | * @param \App\Models\Message $message 88 | * @return mixed 89 | */ 90 | public function forceDelete(User $user, Message $message) 91 | { 92 | // 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Policies/DesignPolicy.php: -------------------------------------------------------------------------------- 1 | user_id === $user->id; 57 | } 58 | 59 | /** 60 | * Determine whether the user can delete the design. 61 | * 62 | * @param \App\Models\User $user 63 | * @param \App\Models\Design $design 64 | * @return mixed 65 | */ 66 | public function delete(User $user, Design $design) 67 | { 68 | return $design->user_id === $user->id; 69 | } 70 | 71 | /** 72 | * Determine whether the user can restore the design. 73 | * 74 | * @param \App\Models\User $user 75 | * @param \App\Models\Design $design 76 | * @return mixed 77 | */ 78 | public function restore(User $user, Design $design) 79 | { 80 | // 81 | } 82 | 83 | /** 84 | * Determine whether the user can permanently delete the design. 85 | * 86 | * @param \App\Models\User $user 87 | * @param \App\Models\Design $design 88 | * @return mixed 89 | */ 90 | public function forceDelete(User $user, Design $design) 91 | { 92 | // 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/LoginController.php: -------------------------------------------------------------------------------- 1 | guard()->attempt($this->credentials($request)); 22 | 23 | if( ! $token){ 24 | return false; 25 | } 26 | 27 | // Get the authenticated user 28 | $user = $this->guard()->user(); 29 | 30 | if($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail()){ 31 | return false; 32 | } 33 | 34 | // set the user's token 35 | $this->guard()->setToken($token); 36 | 37 | return true; 38 | } 39 | 40 | protected function sendLoginResponse(Request $request) 41 | { 42 | $this->clearLoginAttempts($request); 43 | 44 | // get the tokem from the authentication guard (JWT) 45 | $token = (string)$this->guard()->getToken(); 46 | 47 | // extract the expiry date of the token 48 | $expiration = $this->guard()->getPayload()->get('exp'); 49 | 50 | return response()->json([ 51 | 'token' => $token, 52 | 'token_type' => 'bearer', 53 | 'expires_in' => $expiration 54 | ]); 55 | } 56 | 57 | 58 | protected function sendFailedLoginResponse() 59 | { 60 | $user = $this->guard()->user(); 61 | 62 | if($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail()){ 63 | return response()->json(["errors" => [ 64 | "message" => "You need to verify your email account" 65 | ]], 422); 66 | } 67 | 68 | throw ValidationException::withMessages([ 69 | $this->username() => "Invalid credentials" 70 | ]); 71 | } 72 | 73 | public function logout() 74 | { 75 | $this->guard()->logout(); 76 | return response()->json(['message' => 'Logged out successfully!']); 77 | } 78 | 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisterController.php: -------------------------------------------------------------------------------- 1 | users = $users; 22 | } 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Register Controller 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This controller handles the registration of new users as well as their 29 | | validation and creation. By default this controller uses a trait to 30 | | provide this functionality without requiring any additional code. 31 | | 32 | */ 33 | 34 | use RegistersUsers; 35 | 36 | protected function registered(Request $request, User $user) 37 | { 38 | return response()->json($user, 200); 39 | } 40 | 41 | /** 42 | * Get a validator for an incoming registration request. 43 | * 44 | * @param array $data 45 | * @return \Illuminate\Contracts\Validation\Validator 46 | */ 47 | protected function validator(array $data) 48 | { 49 | return Validator::make($data, [ 50 | 'username' => ['required', 'string', 'max:15', 'alpha_dash', 'unique:users,username'], 51 | 'name' => ['required', 'string', 'max:255'], 52 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 53 | 'password' => ['required', 'string', 'min:8', 'confirmed'], 54 | ]); 55 | } 56 | 57 | /** 58 | * Create a new user instance after a valid registration. 59 | * 60 | * @param array $data 61 | * @return \App\User 62 | */ 63 | protected function create(array $data) 64 | { 65 | 66 | return $this->users->create([ 67 | 'username' => $data['username'], 68 | 'name' => $data['name'], 69 | 'email' => $data['email'], 70 | 'password' => Hash::make($data['password']), 71 | ]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Policies/CommentPolicy.php: -------------------------------------------------------------------------------- 1 | user_id == $user->id; 57 | } 58 | 59 | /** 60 | * Determine whether the user can delete the comment. 61 | * 62 | * @param \App\Models\User $user 63 | * @param \App\Models\Comment $comment 64 | * @return mixed 65 | */ 66 | public function delete(User $user, Comment $comment) 67 | { 68 | return $comment->user_id == $user->id; 69 | } 70 | 71 | /** 72 | * Determine whether the user can restore the comment. 73 | * 74 | * @param \App\Models\User $user 75 | * @param \App\Models\Comment $comment 76 | * @return mixed 77 | */ 78 | public function restore(User $user, Comment $comment) 79 | { 80 | // 81 | } 82 | 83 | /** 84 | * Determine whether the user can permanently delete the comment. 85 | * 86 | * @param \App\Models\User $user 87 | * @param \App\Models\Comment $comment 88 | * @return mixed 89 | */ 90 | public function forceDelete(User $user, Comment $comment) 91 | { 92 | // 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerificationController.php: -------------------------------------------------------------------------------- 1 | middleware('throttle:6,1')->only('verify', 'resend'); 28 | $this->users = $users; 29 | } 30 | 31 | public function verify(Request $request, User $user) 32 | { 33 | // check if the url is a valid signed url 34 | if(! URL::hasValidSignature($request)){ 35 | return response()->json(["errors" => [ 36 | "message" => "Invalid verification link or signature" 37 | ]], 422); 38 | } 39 | 40 | // check if the user has already verified account 41 | if($user->hasVerifiedEmail()){ 42 | return response()->json(["errors" => [ 43 | "message" => "Email address already verified" 44 | ]], 422); 45 | } 46 | 47 | $user->markEmailAsVerified(); 48 | event(new Verified($user)); 49 | 50 | return response()->json(['message' => 'Email successfully verified'], 200); 51 | 52 | } 53 | 54 | public function resend(Request $request) 55 | { 56 | $this->validate($request, [ 57 | 'email' => ['email', 'required'] 58 | ]); 59 | 60 | $user = $this->users->findWhereFirst('email', $request->email); 61 | // $user = User::where('email', $request->email)->first(); 62 | 63 | if(! $user){ 64 | return response()->json(["errors" => [ 65 | "email" => "No user could be found with this email address" 66 | ]], 422); 67 | } 68 | 69 | if($user->hasVerifiedEmail()){ 70 | return response()->json(["errors" => [ 71 | "message" => "Email address already verified" 72 | ]], 422); 73 | } 74 | 75 | $user->sendEmailVerificationNotification(); 76 | 77 | return response()->json(['status' => "verification link resent"]); 78 | 79 | } 80 | 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /app/Repositories/Eloquent/DesignRepository.php: -------------------------------------------------------------------------------- 1 | find($id); 20 | $design->retag($data); 21 | } 22 | 23 | public function addComment($designId, array $data) 24 | { 25 | // get the design for which we want to create a comment 26 | $design = $this->find($designId); 27 | 28 | // create the comment for the design 29 | $comment = $design->comments()->create($data); 30 | 31 | return $comment; 32 | } 33 | 34 | public function like($id) 35 | { 36 | $design = $this->model->findOrFail($id); 37 | if($design->isLikedByUser(auth()->id())){ 38 | $design->unlike(); 39 | } else { 40 | $design->like(); 41 | } 42 | 43 | return $design->likes()->count(); 44 | } 45 | 46 | public function isLikedByUser($id) 47 | { 48 | $design = $this->model->findOrFail($id); 49 | return $design->isLikedByUser(auth()->id()); 50 | } 51 | 52 | public function search(Request $request) 53 | { 54 | $query = (new $this->model)->newQuery(); 55 | $query->where('is_live', true); 56 | 57 | // return only designs with comments 58 | if($request->has_comments){ 59 | $query->has('comments'); 60 | } 61 | 62 | // return only designs assigned to teams 63 | if($request->has_team){ 64 | $query->has('team'); 65 | } 66 | 67 | // search title and description for provided string 68 | if($request->q){ 69 | $query->where(function($q) use ($request){ 70 | $q->where('title', 'like', '%'.$request->q.'%') 71 | ->orWhere('description', 'like', '%'.$request->q.'%'); 72 | }); 73 | } 74 | 75 | // order the query by likes or latest first 76 | if($request->orderBy=='likes'){ 77 | $query->withCount('likes') // likes_count 78 | ->orderByDesc('likes_count'); 79 | } else { 80 | $query->latest(); 81 | } 82 | 83 | return $query->with('user')->get(); 84 | } 85 | 86 | 87 | } -------------------------------------------------------------------------------- /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 | 'tmp' => [ 59 | 'driver' => 'local', 60 | 'root' => storage_path(), 61 | 'url' => env('APP_URL').'/storage', 62 | 'visibility' => 'public' 63 | ], 64 | 65 | 's3' => [ 66 | 'driver' => 's3', 67 | 'key' => env('AWS_ACCESS_KEY_ID'), 68 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 69 | 'region' => env('AWS_DEFAULT_REGION'), 70 | 'bucket' => env('AWS_BUCKET'), 71 | 'url' => env('AWS_URL'), 72 | ], 73 | 74 | ], 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /database/migrations/2020_01_25_114136_create_taggable_table.php: -------------------------------------------------------------------------------- 1 | hasTable($taggableTagsTable)) { 21 | Schema::connection($connection)->create($taggableTagsTable, static function(Blueprint $table) { 22 | $table->bigIncrements('tag_id'); 23 | $table->string('name'); 24 | $table->string('normalized')->unique(); 25 | $table->timestamps(); 26 | 27 | $table->index('normalized'); 28 | }); 29 | } 30 | 31 | if (!Schema::connection($connection)->hasTable($taggableTaggablesTable)) { 32 | Schema::connection($connection)->create($taggableTaggablesTable, static function(Blueprint $table) { 33 | $table->unsignedBigInteger('tag_id'); 34 | $table->unsignedBigInteger('taggable_id'); 35 | $table->string('taggable_type'); 36 | $table->timestamps(); 37 | 38 | $table->unique(['tag_id', 'taggable_id', 'taggable_type']); 39 | 40 | $table->index(['tag_id', 'taggable_id'], 'i_taggable_fwd'); 41 | $table->index(['taggable_id', 'tag_id'], 'i_taggable_rev'); 42 | $table->index('taggable_type', 'i_taggable_type'); 43 | }); 44 | } 45 | } 46 | 47 | /** 48 | * Reverse the migrations. 49 | */ 50 | public function down(): void 51 | { 52 | $connection = config('taggable.connection'); 53 | $taggableTagsTable = config('taggable.tables.taggable_tags', 'taggable_tags'); 54 | $taggableTaggablesTable = config('taggable.tables.taggable_taggables', 'taggable_taggables'); 55 | 56 | if (Schema::connection($connection)->hasTable($taggableTagsTable)) { 57 | Schema::connection($connection)->drop($taggableTagsTable); 58 | } 59 | 60 | if (Schema::connection($connection)->hasTable($taggableTaggablesTable)) { 61 | Schema::connection($connection)->drop($taggableTaggablesTable); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /config/taggable.php: -------------------------------------------------------------------------------- 1 | ',;', 10 | 11 | /** 12 | * Character used to delimit tag lists returned in the 13 | * tagList, tagListNormalized, etc. attributes. 14 | */ 15 | 'glue' => ',', 16 | 17 | /** 18 | * Method used to "normalize" tag names. Can either be a global function name, 19 | * a closure function, or a callable, e.g. ['Classname', 'method']. 20 | */ 21 | //'normalizer' => 'mb_strtolower', 22 | 'normalizer' => ['Illuminate\Support\Str', 'slug'], 23 | 24 | 25 | 26 | /** 27 | * The database connection to use for the Tag model and associated tables. 28 | * By default, we use the default database connection, but this can be defined 29 | * so that all the tag-related tables are stored in a different connection. 30 | */ 31 | 'connection' => null, 32 | 33 | /** 34 | * How to handle passing empty values to the scope queries. When set to false, 35 | * the scope queries will return no models. When set to true, passing an empty 36 | * value to the scope queries will throw an exception instead. 37 | */ 38 | 'throwEmptyExceptions' => false, 39 | 40 | /** 41 | * If you want to be able to find all the models that share a tag, you will need 42 | * to define the inverse relations here. The array keys are the relation names 43 | * you would use to access them (e.g. "posts") and the values are the qualified 44 | * class names of the models that are taggable (e.g. "\App\Post). e.g. with 45 | * the following configuration: 46 | * 47 | * 'taggedModels' => [ 48 | * 'posts' => \App\Post::class 49 | * ] 50 | * 51 | * You will be able to do: 52 | * 53 | * $posts = Tag::findByName('Apple')->posts; 54 | * 55 | * to get a collection of all the Posts that are tagged "Apple". 56 | */ 57 | 58 | 'taggedModels' => [], 59 | 60 | /** 61 | * The model used to store the tags in the database. You can 62 | * create your own class that extends the package's Tag model, 63 | * then update the configuration below. 64 | */ 65 | 'model' => \Cviebrock\EloquentTaggable\Models\Tag::class, 66 | 67 | 68 | /** 69 | * The tables used to store the tags in the database. You can 70 | * publish the package's migrations and use custom names. 71 | */ 72 | 'tables' => [ 73 | 'taggable_tags' => 'taggable_tags', 74 | 'taggable_taggables' => 'taggable_taggables', 75 | ] 76 | ]; 77 | -------------------------------------------------------------------------------- /app/Jobs/UploadImage.php: -------------------------------------------------------------------------------- 1 | design = $design; 28 | } 29 | 30 | /** 31 | * Execute the job. 32 | * 33 | * @return void 34 | */ 35 | public function handle() 36 | { 37 | $disk = $this->design->disk; 38 | $filename = $this->design->image; 39 | $original_file = storage_path() . '/uploads/original/'. $filename; 40 | 41 | try{ 42 | // create the Large Image and save to tmp disk 43 | Image::make($original_file) 44 | ->fit(800, 600, function($constraint){ 45 | $constraint->aspectRatio(); 46 | }) 47 | ->save($large = storage_path('uploads/large/'. $filename)); 48 | 49 | // Create the thumbnail image 50 | Image::make($original_file) 51 | ->fit(250, 200, function($constraint){ 52 | $constraint->aspectRatio(); 53 | }) 54 | ->save($thumbnail = storage_path('uploads/thumbnail/'. $filename)); 55 | 56 | // store images to permanent disk 57 | // original image 58 | if(Storage::disk($disk) 59 | ->put('uploads/designs/original/'.$filename, fopen($original_file, 'r+'))){ 60 | File::delete($original_file); 61 | } 62 | 63 | // large images 64 | if(Storage::disk($disk) 65 | ->put('uploads/designs/large/'.$filename, fopen($large, 'r+'))){ 66 | File::delete($large); 67 | } 68 | 69 | // thumbnail images 70 | if(Storage::disk($disk) 71 | ->put('uploads/designs/thumbnail/'.$filename, fopen($thumbnail, 'r+'))){ 72 | File::delete($thumbnail); 73 | } 74 | 75 | // Update the database record with success flag 76 | $this->design->update([ 77 | 'upload_successful' => true 78 | ]); 79 | 80 | } catch(\Exception $e){ 81 | \Log::error($e->getMessage()); 82 | } 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/Http/Controllers/Chats/ChatController.php: -------------------------------------------------------------------------------- 1 | chats = $chats; 21 | $this->messages = $messages; 22 | } 23 | 24 | // Send message to user 25 | public function sendMessage(Request $request) 26 | { 27 | // validate the request 28 | $this->validate($request, [ 29 | 'recipient' => ['required'], 30 | 'body' => ['required'] 31 | ]); 32 | 33 | $recipient = $request->recipient; 34 | $user = auth()->user(); 35 | $body = $request->body; 36 | 37 | // check if there is an existing chat 38 | // between the auth user and the recipient 39 | $chat = $user->getChatWithUser($recipient); 40 | 41 | if(! $chat){ 42 | $chat = $this->chats->create([]); 43 | $this->chats->createParticipants($chat->id, [$user->id, $recipient]); 44 | } 45 | 46 | // add the message to the chat 47 | $message = $this->messages->create([ 48 | 'user_id' => $user->id, 49 | 'chat_id' => $chat->id, 50 | 'body' => $body, 51 | 'last_read' => null 52 | ]); 53 | 54 | return new MessageResource($message); 55 | 56 | } 57 | 58 | // Get chats for user 59 | public function getUserChats() 60 | { 61 | $chats = $this->chats->getUserChats(); 62 | return ChatResource::collection($chats); 63 | } 64 | 65 | // get messages for chat 66 | public function getChatMessages($id) 67 | { 68 | $messages = $this->messages->withCriteria([ 69 | new WithTrashed() 70 | ])->findWhere('chat_id', $id); 71 | 72 | return MessageResource::collection($messages); 73 | } 74 | 75 | // mark chat as read 76 | public function markAsRead($id) 77 | { 78 | $chat = $this->chats->find($id); 79 | $chat->markAsReadForUser(auth()->id()); 80 | return response()->json(['message' => 'successful'], 200); 81 | } 82 | 83 | // destroy message 84 | public function destroyMessage($id) 85 | { 86 | $message = $this->messages->find($id); 87 | $this->authorize('delete', $message); 88 | $message->delete(); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /app/Policies/InvitationPolicy.php: -------------------------------------------------------------------------------- 1 | id == $invitation->sender_id; 69 | } 70 | 71 | public function resend(User $user, Invitation $invitation) 72 | { 73 | return $user->id == $invitation->sender_id; 74 | } 75 | 76 | 77 | public function respond(User $user, Invitation $invitation) 78 | { 79 | return $user->email == $invitation->recipient_email; 80 | } 81 | 82 | 83 | 84 | /** 85 | * Determine whether the user can restore the invitation. 86 | * 87 | * @param \App\Models\User $user 88 | * @param \App\Models\Invitation $invitation 89 | * @return mixed 90 | */ 91 | public function restore(User $user, Invitation $invitation) 92 | { 93 | // 94 | } 95 | 96 | /** 97 | * Determine whether the user can permanently delete the invitation. 98 | * 99 | * @param \App\Models\User $user 100 | * @param \App\Models\Invitation $invitation 101 | * @return mixed 102 | */ 103 | public function forceDelete(User $user, Invitation $invitation) 104 | { 105 | // 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /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 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 59 | ], 60 | 61 | 'redis' => [ 62 | 'driver' => 'redis', 63 | 'connection' => 'default', 64 | 'queue' => env('REDIS_QUEUE', 'default'), 65 | 'retry_after' => 90, 66 | 'block_for' => null, 67 | ], 68 | 69 | ], 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Failed Queue Jobs 74 | |-------------------------------------------------------------------------- 75 | | 76 | | These options configure the behavior of failed queue job logging so you 77 | | can control which database and table are used to store the jobs that 78 | | have failed. You may change them to any database / table you wish. 79 | | 80 | */ 81 | 82 | 'failed' => [ 83 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database'), 84 | 'database' => env('DB_CONNECTION', 'mysql'), 85 | 'table' => 'failed_jobs', 86 | ], 87 | 88 | ]; 89 | -------------------------------------------------------------------------------- /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' => 'debug', 48 | ], 49 | 50 | 'daily' => [ 51 | 'driver' => 'daily', 52 | 'path' => storage_path('logs/laravel.log'), 53 | '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' => 'critical', 63 | ], 64 | 65 | 'papertrail' => [ 66 | 'driver' => 'monolog', 67 | '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' => 'debug', 87 | ], 88 | 89 | 'errorlog' => [ 90 | 'driver' => 'errorlog', 91 | 'level' => 'debug', 92 | ], 93 | 94 | 'null' => [ 95 | 'driver' => 'monolog', 96 | 'handler' => NullHandler::class, 97 | ], 98 | ], 99 | 100 | ]; 101 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 37 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 38 | \App\Http\Middleware\VerifyCsrfToken::class, 39 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 40 | ], 41 | 42 | 'api' => [ 43 | 'throttle:60,1', 44 | 'bindings', 45 | ], 46 | ]; 47 | 48 | /** 49 | * The application's route middleware. 50 | * 51 | * These middleware may be assigned to groups or used individually. 52 | * 53 | * @var array 54 | */ 55 | protected $routeMiddleware = [ 56 | 'auth' => \App\Http\Middleware\Authenticate::class, 57 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 58 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 59 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 60 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 61 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 62 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 63 | 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 64 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 65 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 66 | ]; 67 | 68 | /** 69 | * The priority-sorted list of middleware. 70 | * 71 | * This forces non-global middleware to always be in the given order. 72 | * 73 | * @var array 74 | */ 75 | protected $middlewarePriority = [ 76 | \Illuminate\Session\Middleware\StartSession::class, 77 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 78 | \App\Http\Middleware\Authenticate::class, 79 | \Illuminate\Routing\Middleware\ThrottleRequests::class, 80 | \Illuminate\Session\Middleware\AuthenticateSession::class, 81 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 82 | \Illuminate\Auth\Middleware\Authorize::class, 83 | ]; 84 | } 85 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Laravel 8 | 9 | 10 | 11 | 12 | 13 | 65 | 66 | 67 |
68 | @if (Route::has('login')) 69 | 80 | @endif 81 | 82 |
83 |
84 | Laravel 85 |
86 | 87 | 97 |
98 |
99 | 100 | 101 | -------------------------------------------------------------------------------- /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 | ], 43 | 44 | 'database' => [ 45 | 'driver' => 'database', 46 | 'table' => 'cache', 47 | 'connection' => null, 48 | ], 49 | 50 | 'file' => [ 51 | 'driver' => 'file', 52 | 'path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => 'cache', 77 | ], 78 | 79 | 'dynamodb' => [ 80 | 'driver' => 'dynamodb', 81 | 'key' => env('AWS_ACCESS_KEY_ID'), 82 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 83 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 84 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 85 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 86 | ], 87 | 88 | ], 89 | 90 | /* 91 | |-------------------------------------------------------------------------- 92 | | Cache Key Prefix 93 | |-------------------------------------------------------------------------- 94 | | 95 | | When utilizing a RAM based store such as APC or Memcached, there might 96 | | be other applications utilizing the same cache. So, we'll specify a 97 | | value to get prefixed to all our keys so we can avoid collisions. 98 | | 99 | */ 100 | 101 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), 102 | 103 | ]; 104 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | ['auth:api']], function(){ 27 | Route::post('logout', 'Auth\LoginController@logout'); 28 | Route::put('settings/profile', 'User\SettingsController@updateProfile'); 29 | Route::put('settings/password', 'User\SettingsController@updatePassword'); 30 | 31 | 32 | // Upload Designs 33 | Route::post('designs', 'Designs\UploadController@upload'); 34 | Route::put('designs/{id}', 'Designs\DesignController@update'); 35 | Route::get('designs/{id}/byUser', 'Designs\DesignController@userOwnsDesign'); 36 | 37 | Route::delete('designs/{id}', 'Designs\DesignController@destroy'); 38 | 39 | // Likes and Unlikes 40 | Route::post('designs/{id}/like', 'Designs\DesignController@like'); 41 | Route::get('designs/{id}/liked', 'Designs\DesignController@checkIfUserHasLiked'); 42 | 43 | // Comments 44 | Route::post('designs/{id}/comments', 'Designs\CommentController@store'); 45 | Route::put('comments/{id}', 'Designs\CommentController@update'); 46 | Route::delete('comments/{id}', 'Designs\CommentController@destroy'); 47 | 48 | // Teams 49 | Route::post('teams', 'Teams\TeamsController@store'); 50 | Route::get('teams/{id}', 'Teams\TeamsController@findById'); 51 | Route::get('teams', 'Teams\TeamsController@index'); 52 | Route::get('users/teams', 'Teams\TeamsController@fetchUserTeams'); 53 | Route::put('teams/{id}', 'Teams\TeamsController@update'); 54 | Route::delete('teams/{id}', 'Teams\TeamsController@destroy'); 55 | Route::delete('teams/{team_id}/users/{user_id}', 'Teams\TeamsController@removeFromTeam'); 56 | 57 | // Invitations 58 | Route::post('invitations/{teamId}', 'Teams\InvitationsController@invite'); 59 | Route::post('invitations/{id}/resend', 'Teams\InvitationsController@resend'); 60 | Route::post('invitations/{id}/respond', 'Teams\InvitationsController@respond'); 61 | Route::delete('invitations/{id}', 'Teams\InvitationsController@destroy'); 62 | 63 | // Chats 64 | Route::post('chats', 'Chats\ChatController@sendMessage'); 65 | Route::get('chats', 'Chats\ChatController@getUserChats'); 66 | Route::get('chats/{id}/messages', 'Chats\ChatController@getChatMessages'); 67 | Route::put('chats/{id}/markAsRead', 'Chats\ChatController@markAsRead'); 68 | Route::delete('messages/{id}', 'Chats\ChatController@destroyMessage'); 69 | 70 | }); 71 | 72 | // Routes for guests only 73 | Route::group(['middleware' => ['guest:api']], function(){ 74 | Route::post('register', 'Auth\RegisterController@register'); 75 | Route::post('verification/verify/{user}', 'Auth\VerificationController@verify')->name('verification.verify'); 76 | Route::post('verification/resend', 'Auth\VerificationController@resend'); 77 | Route::post('login', 'Auth\LoginController@login'); 78 | Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail'); 79 | Route::post('password/reset', 'Auth\ResetPasswordController@reset'); 80 | 81 | 82 | 83 | 84 | }); 85 | 86 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | email)).'.jpg?s=200&d=mm'; 54 | } 55 | 56 | 57 | /** 58 | * The attributes that should be cast to native types. 59 | * 60 | * @var array 61 | */ 62 | protected $casts = [ 63 | 'email_verified_at' => 'datetime', 64 | 'available_to_hire' => 'boolean' 65 | ]; 66 | 67 | public function sendEmailVerificationNotification() 68 | { 69 | $this->notify(new VerifyEmail); 70 | } 71 | 72 | public function sendPasswordResetNotification($token) 73 | { 74 | $this->notify(new ResetPassword($token)); 75 | } 76 | 77 | 78 | public function designs() 79 | { 80 | return $this->hasMany(Design::class); 81 | } 82 | 83 | public function comments() 84 | { 85 | return $this->hasMany(Comment::class); 86 | } 87 | 88 | // teams that the user belongs to 89 | public function teams() 90 | { 91 | return $this->belongsToMany(Team::class) 92 | ->withTimestamps(); 93 | } 94 | 95 | public function ownedTeams() 96 | { 97 | return $this->teams() 98 | ->where('owner_id', $this->id); 99 | } 100 | 101 | public function isOwnerOfTeam($team) 102 | { 103 | return (bool)$this->teams() 104 | ->where('id', $team->id) 105 | ->where('owner_id', $this->id) 106 | ->count(); 107 | } 108 | 109 | 110 | // Relationships for invitations 111 | public function invitations() 112 | { 113 | return $this->hasMany(Invitation::class, 'recipient_email', 'email'); 114 | } 115 | 116 | // relationships for chat messaging 117 | public function chats() 118 | { 119 | return $this->belongsToMany(Chat::class, 'participants'); 120 | } 121 | 122 | public function messages() 123 | { 124 | return $this->hasMany(Message::class); 125 | } 126 | 127 | public function getChatWithUser($user_id) 128 | { 129 | $chat = $this->chats() 130 | ->whereHas('participants', function($query) use ($user_id){ 131 | $query->where('user_id', $user_id); 132 | }) 133 | ->first(); 134 | return $chat; 135 | } 136 | 137 | 138 | 139 | 140 | 141 | /** 142 | * Get the identifier that will be stored in the subject claim of the JWT. 143 | * 144 | * @return mixed 145 | */ 146 | public function getJWTIdentifier() 147 | { 148 | return $this->getKey(); 149 | } 150 | 151 | /** 152 | * Return a key value array, containing any custom claims to be added to the JWT. 153 | * 154 | * @return array 155 | */ 156 | public function getJWTCustomClaims() 157 | { 158 | return []; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => 'api', 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' => 'jwt', 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 | -------------------------------------------------------------------------------- /app/Http/Controllers/Teams/TeamsController.php: -------------------------------------------------------------------------------- 1 | teams = $teams; 24 | $this->users = $users; 25 | $this->invitations = $invitations; 26 | } 27 | 28 | /** 29 | * Get list of all teams (eg for Search) 30 | */ 31 | public function index(Request $request) 32 | { 33 | 34 | } 35 | 36 | /** 37 | * Save team to database 38 | */ 39 | public function store(Request $request) 40 | { 41 | $this->validate($request, [ 42 | 'name' => ['required', 'string', 'max:80', 'unique:teams,name'] 43 | ]); 44 | 45 | // create team in database 46 | $team = $this->teams->create([ 47 | 'owner_id' => auth()->id(), 48 | 'name' => $request->name, 49 | 'slug' => Str::slug($request->name) 50 | ]); 51 | 52 | // current user is inserted as 53 | // team member using boot method in Team model 54 | 55 | return new TeamResource($team); 56 | 57 | 58 | } 59 | 60 | /** 61 | * Update team information 62 | */ 63 | public function update(Request $request, $id) 64 | { 65 | $team = $this->teams->find($id); 66 | $this->authorize('update', $team); 67 | 68 | $this->validate($request, [ 69 | 'name' => ['required', 'string', 'max:80', 'unique:teams,name,'.$id] 70 | ]); 71 | 72 | $team = $this->teams->update($id, [ 73 | 'name' => $request->name, 74 | 'slug' => Str::slug($request->name) 75 | ]); 76 | 77 | return new TeamResource($team); 78 | } 79 | 80 | /** 81 | * Find a team by its ID 82 | */ 83 | public function findById($id) 84 | { 85 | $team = $this->teams->find($id); 86 | return new TeamResource($team); 87 | } 88 | 89 | /** 90 | * Get the teams that the current user belongs to 91 | */ 92 | public function fetchUserTeams() 93 | { 94 | $teams = $this->teams->fetchUserTeams(); 95 | return TeamResource::collection($teams); 96 | } 97 | 98 | /** 99 | * Get team by slug for Public view 100 | */ 101 | public function findBySlug($slug) 102 | { 103 | $team = $this->teams->findWhereFirst('slug', $slug); 104 | return new TeamResource($team); 105 | } 106 | 107 | /** 108 | * Destroy (delete) a team 109 | */ 110 | public function destroy($id) 111 | { 112 | $team = $this->teams->find($id); 113 | $this->authorize('delete', $team); 114 | 115 | $team->delete(); 116 | 117 | return response()->json(['message' => 'Deleted'], 200); 118 | } 119 | 120 | public function removeFromTeam($teamId, $userId) 121 | { 122 | // get the team 123 | $team = $this->teams->find($teamId); 124 | $user = $this->users->find($userId); 125 | 126 | // check that the user is not the owner 127 | if($user->isOwnerOfTeam($team)){ 128 | return response()->json([ 129 | 'message' => 'You are the team owner' 130 | ], 401); 131 | } 132 | 133 | // check that the person sending the request 134 | // is either the owner of the team or the person 135 | // who wants to leave the team 136 | if(!auth()->user()->isOwnerOfTeam($team) && 137 | auth()->id() !== $user->id 138 | ){ 139 | return response()->json([ 140 | 'message' => 'You cannot do this' 141 | ], 401); 142 | } 143 | 144 | $this->invitations->removeUserFromTeam($team, $userId); 145 | 146 | return response()->json(['message' => 'Success'], 200); 147 | 148 | 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 |

9 | 10 | ## About Laravel 11 | 12 | Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: 13 | 14 | - [Simple, fast routing engine](https://laravel.com/docs/routing). 15 | - [Powerful dependency injection container](https://laravel.com/docs/container). 16 | - Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. 17 | - Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). 18 | - Database agnostic [schema migrations](https://laravel.com/docs/migrations). 19 | - [Robust background job processing](https://laravel.com/docs/queues). 20 | - [Real-time event broadcasting](https://laravel.com/docs/broadcasting). 21 | 22 | Laravel is accessible, powerful, and provides tools required for large, robust applications. 23 | 24 | ## Learning Laravel 25 | 26 | Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. 27 | 28 | If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 1500 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. 29 | 30 | ## Laravel Sponsors 31 | 32 | We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell). 33 | 34 | - **[Vehikl](https://vehikl.com/)** 35 | - **[Tighten Co.](https://tighten.co)** 36 | - **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** 37 | - **[64 Robots](https://64robots.com)** 38 | - **[Cubet Techno Labs](https://cubettech.com)** 39 | - **[Cyber-Duck](https://cyber-duck.co.uk)** 40 | - **[British Software Development](https://www.britishsoftware.co)** 41 | - **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)** 42 | - **[DevSquad](https://devsquad.com)** 43 | - [UserInsights](https://userinsights.com) 44 | - [Fragrantica](https://www.fragrantica.com) 45 | - [SOFTonSOFA](https://softonsofa.com/) 46 | - [User10](https://user10.com) 47 | - [Soumettre.fr](https://soumettre.fr/) 48 | - [CodeBrisk](https://codebrisk.com) 49 | - [1Forge](https://1forge.com) 50 | - [TECPRESSO](https://tecpresso.co.jp/) 51 | - [Runtime Converter](http://runtimeconverter.com/) 52 | - [WebL'Agence](https://weblagence.com/) 53 | - [Invoice Ninja](https://www.invoiceninja.com) 54 | - [iMi digital](https://www.imi-digital.de/) 55 | - [Earthlink](https://www.earthlink.ro/) 56 | - [Steadfast Collective](https://steadfastcollective.com/) 57 | - [We Are The Robots Inc.](https://watr.mx/) 58 | - [Understand.io](https://www.understand.io/) 59 | - [Abdel Elrafa](https://abdelelrafa.com) 60 | - [Hyper Host](https://hyper.host) 61 | - [Appoly](https://www.appoly.co.uk) 62 | - [OP.GG](https://op.gg) 63 | 64 | ## Contributing 65 | 66 | Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). 67 | 68 | ## Code of Conduct 69 | 70 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 71 | 72 | ## Security Vulnerabilities 73 | 74 | If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. 75 | 76 | ## License 77 | 78 | The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 79 | -------------------------------------------------------------------------------- /app/Http/Controllers/Designs/DesignController.php: -------------------------------------------------------------------------------- 1 | designs = $designs; 26 | } 27 | 28 | public function index() 29 | { 30 | $designs = $this->designs->withCriteria([ 31 | new LatestFirst(), 32 | new IsLive(), 33 | new ForUser(2), 34 | new EagerLoad(['user', 'comments']) 35 | ])->all(); 36 | return DesignResource::collection($designs); 37 | } 38 | 39 | public function findDesign($id) 40 | { 41 | $design = $this->designs->find($id); 42 | return new DesignResource($design); 43 | } 44 | 45 | public function update(Request $request, $id) 46 | { 47 | 48 | $design = $this->designs->find($id); 49 | 50 | $this->authorize('update', $design); 51 | $this->validate($request, [ 52 | 'title' => ['required', 'unique:designs,title,'. $id], 53 | 'description' => ['required', 'string', 'min:20', 'max:140'], 54 | 'tags' => ['required'], 55 | 'team' => ['required_if:assign_to_team,true'] 56 | ]); 57 | 58 | 59 | $design = $this->designs->update($id, [ 60 | 'team_id' => $request->team, 61 | 'title' => $request->title, 62 | 'description' => $request->description, 63 | 'slug' => Str::slug($request->title), 64 | 'is_live' => ! $design->upload_successful ? false : $request->is_live 65 | ]); 66 | 67 | // apply the tags 68 | $this->designs->applyTags($id, $request->tags); 69 | 70 | return new DesignResource($design); 71 | } 72 | 73 | public function destroy($id) 74 | { 75 | $design = $this->designs->find($id); 76 | $this->authorize('delete', $design); 77 | // delete the files associated to the record 78 | foreach(['thumbnail', 'large', 'original'] as $size){ 79 | // check if the file exists in the database 80 | if(Storage::disk($design->disk)->exists("uploads/designs/{$size}/".$design->image)){ 81 | Storage::disk($design->disk)->delete("uploads/designs/{$size}/".$design->image); 82 | } 83 | } 84 | $this->designs->delete($id); 85 | return response()->json(['message' => 'Record deleted'], 200); 86 | 87 | } 88 | 89 | public function like($id) 90 | { 91 | $total = $this->designs->like($id); 92 | return response()->json([ 93 | 'message' => 'Successful', 94 | 'total' => $total 95 | ], 200); 96 | } 97 | 98 | public function checkIfUserHasLiked($designId) 99 | { 100 | $isLiked = $this->designs->isLikedByUser($designId); 101 | return response()->json(['liked' => $isLiked], 200); 102 | } 103 | 104 | public function search(Request $request) 105 | { 106 | $designs = $this->designs->search($request); 107 | return DesignResource::collection($designs); 108 | } 109 | 110 | public function findBySlug($slug) 111 | { 112 | $design = $this->designs->withCriteria([ 113 | new IsLive(), 114 | new EagerLoad(['user', 'comments']) 115 | ])->findWhereFirst('slug', $slug); 116 | return new DesignResource($design); 117 | } 118 | 119 | public function getForTeam($teamId) 120 | { 121 | $designs = $this->designs 122 | ->withCriteria([new IsLive()]) 123 | ->findWhere('team_id', $teamId); 124 | return DesignResource::collection($designs); 125 | } 126 | 127 | public function getForUser($userId) 128 | { 129 | $designs = $this->designs 130 | //->withCriteria([new IsLive()]) 131 | ->findWhere('user_id', $userId); 132 | return DesignResource::collection($designs); 133 | } 134 | 135 | public function userOwnsDesign($id) 136 | { 137 | $design = $this->designs->withCriteria( 138 | [ new ForUser(auth()->id())] 139 | )->findWhereFirst('id', $id); 140 | 141 | return new DesignResource($design); 142 | } 143 | 144 | 145 | } 146 | -------------------------------------------------------------------------------- /app/Http/Controllers/Teams/InvitationsController.php: -------------------------------------------------------------------------------- 1 | invitations = $invitations; 27 | $this->teams = $teams; 28 | $this->users = $users; 29 | } 30 | 31 | public function invite(Request $request, $teamId) 32 | { 33 | // get the team 34 | $team = $this->teams->find($teamId); 35 | 36 | $this->validate($request, [ 37 | 'email' => ['required', 'email'] 38 | ]); 39 | $user = auth()->user(); 40 | // check if the user owns the team 41 | if(! $user->isOwnerOfTeam($team)){ 42 | return response()->json([ 43 | 'email' => 'You are not the team owner' 44 | ], 401); 45 | } 46 | 47 | // check if the email has a pending invitation 48 | if($team->hasPendingInvite($request->email)){ 49 | return response()->json([ 50 | 'email' => 'Email already has a pending invite' 51 | ], 422); 52 | } 53 | 54 | // get the recipient by email 55 | $recipient = $this->users->findByEmail($request->email); 56 | 57 | // if the recipient does not exist, send invitation to join the team 58 | if(! $recipient){ 59 | $this->createInvitation(false, $team, $request->email); 60 | 61 | return response()->json([ 62 | 'message' => 'Invitation sent to user' 63 | ], 200); 64 | } 65 | 66 | // check if the team already has the user 67 | if($team->hasUser($recipient)){ 68 | return response()->json([ 69 | 'email' => 'This user seems to be a team member already' 70 | ], 422); 71 | } 72 | 73 | // send the invitation to the user 74 | $this->createInvitation(true, $team, $request->email); 75 | return response()->json([ 76 | 'message' => 'Invitation sent to user' 77 | ], 200); 78 | } 79 | 80 | public function resend($id) 81 | { 82 | $invitation = $this->invitations->find($id); 83 | 84 | $this->authorize('resend', $invitation); 85 | 86 | $recipient = $this->users 87 | ->findByEmail($invitation->recipient_email); 88 | 89 | Mail::to($invitation->recipient_email) 90 | ->send(new SendInvitationToJoinTeam($invitation, !is_null($recipient))); 91 | 92 | return response()->json(['message' => 'Invitation resent'], 200); 93 | } 94 | 95 | public function respond(Request $request, $id) 96 | { 97 | $this->validate($request, [ 98 | 'token' => ['required'], 99 | 'decision' => ['required'] 100 | ]); 101 | 102 | $token = $request->token; 103 | $decision = $request->decision; // 'accept' or 'deny' 104 | $invitation = $this->invitations->find($id); 105 | 106 | // check if the invitation belongs to this user 107 | $this->authorize('respond', $invitation); 108 | 109 | 110 | // check to make sure that the tokens match 111 | if($invitation->token !== $token){ 112 | return response()->json([ 113 | 'message' => 'Invalid Token' 114 | ], 401); 115 | } 116 | 117 | // check if accepted 118 | if($decision !== 'deny'){ 119 | $this->invitations->addUserToTeam($invitation->team, auth()->id()); 120 | } 121 | 122 | $invitation->delete(); 123 | 124 | return response()->json(['message' => 'Successful'], 200); 125 | 126 | } 127 | 128 | public function destroy($id) 129 | { 130 | $invitation = $this->invitations->find($id); 131 | $this->authorize('delete', $invitation); 132 | 133 | $invitation->delete(); 134 | 135 | return response()->json(['message' => 'Deleted'], 200); 136 | } 137 | 138 | protected function createInvitation(bool $user_exists, Team $team, string $email) 139 | { 140 | 141 | $invitation = $this->invitations->create([ 142 | 'team_id' => $team->id, 143 | 'sender_id' => auth()->id(), 144 | 'recipient_email' => $email, 145 | 'token' => md5(uniqid(microtime())) 146 | ]); 147 | 148 | Mail::to($email) 149 | ->send(new SendInvitationToJoinTeam($invitation, $user_exists)); 150 | 151 | } 152 | 153 | 154 | 155 | 156 | } 157 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_DRIVER', 'smtp'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | SMTP Host Address 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may provide the host address of the SMTP server used by your 27 | | applications. A default option is provided that is compatible with 28 | | the Mailgun mail service which will provide reliable deliveries. 29 | | 30 | */ 31 | 32 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | SMTP Host Port 37 | |-------------------------------------------------------------------------- 38 | | 39 | | This is the SMTP port used by your application to deliver e-mails to 40 | | users of the application. Like the host we have set this value to 41 | | stay compatible with the Mailgun e-mail application by default. 42 | | 43 | */ 44 | 45 | 'port' => env('MAIL_PORT', 587), 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Global "From" Address 50 | |-------------------------------------------------------------------------- 51 | | 52 | | You may wish for all e-mails sent by your application to be sent from 53 | | the same address. Here, you may specify a name and address that is 54 | | used globally for all e-mails that are sent by your application. 55 | | 56 | */ 57 | 58 | 'from' => [ 59 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 60 | 'name' => env('MAIL_FROM_NAME', 'Example'), 61 | ], 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | E-Mail Encryption Protocol 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Here you may specify the encryption protocol that should be used when 69 | | the application send e-mail messages. A sensible default using the 70 | | transport layer security protocol should provide great security. 71 | | 72 | */ 73 | 74 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | SMTP Server Username 79 | |-------------------------------------------------------------------------- 80 | | 81 | | If your SMTP server requires a username for authentication, you should 82 | | set it here. This will get used to authenticate with your server on 83 | | connection. You may also set the "password" value below this one. 84 | | 85 | */ 86 | 87 | 'username' => env('MAIL_USERNAME'), 88 | 89 | 'password' => env('MAIL_PASSWORD'), 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Sendmail System Path 94 | |-------------------------------------------------------------------------- 95 | | 96 | | When using the "sendmail" driver to send e-mails, we will need to know 97 | | the path to where Sendmail lives on this server. A default path has 98 | | been provided here, which will work well on most of your systems. 99 | | 100 | */ 101 | 102 | 'sendmail' => '/usr/sbin/sendmail -bs', 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | Markdown Mail Settings 107 | |-------------------------------------------------------------------------- 108 | | 109 | | If you are using Markdown based email rendering, you may configure your 110 | | theme and component paths here, allowing you to customize the design 111 | | of the emails. Or, you may simply stick with the Laravel defaults! 112 | | 113 | */ 114 | 115 | 'markdown' => [ 116 | 'theme' => 'default', 117 | 118 | 'paths' => [ 119 | resource_path('views/vendor/mail'), 120 | ], 121 | ], 122 | 123 | /* 124 | |-------------------------------------------------------------------------- 125 | | Log Channel 126 | |-------------------------------------------------------------------------- 127 | | 128 | | If you are using the "log" driver, you may specify the logging channel 129 | | if you prefer to keep mail messages separate from other log entries 130 | | for simpler reading. Otherwise, the default channel will be used. 131 | | 132 | */ 133 | 134 | 'log_channel' => env('MAIL_LOG_CHANNEL'), 135 | 136 | ]; 137 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'mysql'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Database Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here are each of the database connections setup for your application. 26 | | Of course, examples of configuring each database platform that is 27 | | supported by Laravel is shown below to make development simple. 28 | | 29 | | 30 | | All database work in Laravel is done through the PHP PDO facilities 31 | | so make sure you have the driver for your particular database of 32 | | choice installed on your machine before you begin development. 33 | | 34 | */ 35 | 36 | 'connections' => [ 37 | 38 | 'sqlite' => [ 39 | 'driver' => 'sqlite', 40 | 'url' => env('DATABASE_URL'), 41 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 42 | 'prefix' => '', 43 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 44 | ], 45 | 46 | 'mysql' => [ 47 | 'driver' => 'mysql', 48 | 'url' => env('DATABASE_URL'), 49 | 'host' => env('DB_HOST', '127.0.0.1'), 50 | 'port' => env('DB_PORT', '3306'), 51 | 'database' => env('DB_DATABASE', 'forge'), 52 | 'username' => env('DB_USERNAME', 'forge'), 53 | 'password' => env('DB_PASSWORD', ''), 54 | 'unix_socket' => env('DB_SOCKET', ''), 55 | 'charset' => 'utf8mb4', 56 | 'collation' => 'utf8mb4_unicode_ci', 57 | 'prefix' => '', 58 | 'prefix_indexes' => true, 59 | 'strict' => true, 60 | 'engine' => null, 61 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 62 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 63 | ]) : [], 64 | ], 65 | 66 | 'pgsql' => [ 67 | 'driver' => 'pgsql', 68 | 'url' => env('DATABASE_URL'), 69 | 'host' => env('DB_HOST', '127.0.0.1'), 70 | 'port' => env('DB_PORT', '5432'), 71 | 'database' => env('DB_DATABASE', 'forge'), 72 | 'username' => env('DB_USERNAME', 'forge'), 73 | 'password' => env('DB_PASSWORD', ''), 74 | 'charset' => 'utf8', 75 | 'prefix' => '', 76 | 'prefix_indexes' => true, 77 | 'schema' => 'public', 78 | 'sslmode' => 'prefer', 79 | ], 80 | 81 | 'sqlsrv' => [ 82 | 'driver' => 'sqlsrv', 83 | 'url' => env('DATABASE_URL'), 84 | 'host' => env('DB_HOST', 'localhost'), 85 | 'port' => env('DB_PORT', '1433'), 86 | 'database' => env('DB_DATABASE', 'forge'), 87 | 'username' => env('DB_USERNAME', 'forge'), 88 | 'password' => env('DB_PASSWORD', ''), 89 | 'charset' => 'utf8', 90 | 'prefix' => '', 91 | 'prefix_indexes' => true, 92 | ], 93 | 94 | ], 95 | 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | Migration Repository Table 99 | |-------------------------------------------------------------------------- 100 | | 101 | | This table keeps track of all the migrations that have already run for 102 | | your application. Using this information, we can determine which of 103 | | the migrations on disk haven't actually been run in the database. 104 | | 105 | */ 106 | 107 | 'migrations' => 'migrations', 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | Redis Databases 112 | |-------------------------------------------------------------------------- 113 | | 114 | | Redis is an open source, fast, and advanced key-value store that also 115 | | provides a richer body of commands than a typical key-value system 116 | | such as APC or Memcached. Laravel makes it easy to dig right in. 117 | | 118 | */ 119 | 120 | 'redis' => [ 121 | 122 | 'client' => env('REDIS_CLIENT', 'phpredis'), 123 | 124 | 'options' => [ 125 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 126 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 127 | ], 128 | 129 | 'default' => [ 130 | 'url' => env('REDIS_URL'), 131 | 'host' => env('REDIS_HOST', '127.0.0.1'), 132 | 'password' => env('REDIS_PASSWORD', null), 133 | 'port' => env('REDIS_PORT', 6379), 134 | 'database' => env('REDIS_DB', 0), 135 | ], 136 | 137 | 'cache' => [ 138 | 'url' => env('REDIS_URL'), 139 | 'host' => env('REDIS_HOST', '127.0.0.1'), 140 | 'password' => env('REDIS_PASSWORD', null), 141 | 'port' => env('REDIS_PORT', 6379), 142 | 'database' => env('REDIS_CACHE_DB', 1), 143 | ], 144 | 145 | ], 146 | 147 | ]; 148 | -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | env('SESSION_DRIVER', 'cookie'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Session Lifetime 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Here you may specify the number of minutes that you wish the session 29 | | to be allowed to remain idle before it expires. If you want them 30 | | to immediately expire on the browser closing, set that option. 31 | | 32 | */ 33 | 34 | 'lifetime' => env('SESSION_LIFETIME', 120), 35 | 36 | 'expire_on_close' => false, 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Session Encryption 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This option allows you to easily specify that all of your session data 44 | | should be encrypted before it is stored. All encryption will be run 45 | | automatically by Laravel and you can use the Session like normal. 46 | | 47 | */ 48 | 49 | 'encrypt' => false, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Session File Location 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When using the native session driver, we need a location where session 57 | | files may be stored. A default has been set for you but a different 58 | | location may be specified. This is only needed for file sessions. 59 | | 60 | */ 61 | 62 | 'files' => storage_path('framework/sessions'), 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Session Database Connection 67 | |-------------------------------------------------------------------------- 68 | | 69 | | When using the "database" or "redis" session drivers, you may specify a 70 | | connection that should be used to manage these sessions. This should 71 | | correspond to a connection in your database configuration options. 72 | | 73 | */ 74 | 75 | 'connection' => env('SESSION_CONNECTION', null), 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Session Database Table 80 | |-------------------------------------------------------------------------- 81 | | 82 | | When using the "database" session driver, you may specify the table we 83 | | should use to manage the sessions. Of course, a sensible default is 84 | | provided for you; however, you are free to change this as needed. 85 | | 86 | */ 87 | 88 | 'table' => 'sessions', 89 | 90 | /* 91 | |-------------------------------------------------------------------------- 92 | | Session Cache Store 93 | |-------------------------------------------------------------------------- 94 | | 95 | | When using the "apc", "memcached", or "dynamodb" session drivers you may 96 | | list a cache store that should be used for these sessions. This value 97 | | must match with one of the application's configured cache "stores". 98 | | 99 | */ 100 | 101 | 'store' => env('SESSION_STORE', null), 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Session Sweeping Lottery 106 | |-------------------------------------------------------------------------- 107 | | 108 | | Some session drivers must manually sweep their storage location to get 109 | | rid of old sessions from storage. Here are the chances that it will 110 | | happen on a given request. By default, the odds are 2 out of 100. 111 | | 112 | */ 113 | 114 | 'lottery' => [2, 100], 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Session Cookie Name 119 | |-------------------------------------------------------------------------- 120 | | 121 | | Here you may change the name of the cookie used to identify a session 122 | | instance by ID. The name specified here will get used every time a 123 | | new session cookie is created by the framework for every driver. 124 | | 125 | */ 126 | 127 | 'cookie' => env( 128 | 'SESSION_COOKIE', 129 | Str::slug(env('APP_NAME', 'laravel'), '_').'_session' 130 | ), 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Session Cookie Path 135 | |-------------------------------------------------------------------------- 136 | | 137 | | The session cookie path determines the path for which the cookie will 138 | | be regarded as available. Typically, this will be the root path of 139 | | your application but you are free to change this when necessary. 140 | | 141 | */ 142 | 143 | 'path' => '/', 144 | 145 | /* 146 | |-------------------------------------------------------------------------- 147 | | Session Cookie Domain 148 | |-------------------------------------------------------------------------- 149 | | 150 | | Here you may change the domain of the cookie used to identify a session 151 | | in your application. This will determine which domains the cookie is 152 | | available to in your application. A sensible default has been set. 153 | | 154 | */ 155 | 156 | 'domain' => env('SESSION_DOMAIN', null), 157 | 158 | /* 159 | |-------------------------------------------------------------------------- 160 | | HTTPS Only Cookies 161 | |-------------------------------------------------------------------------- 162 | | 163 | | By setting this option to true, session cookies will only be sent back 164 | | to the server if the browser has a HTTPS connection. This will keep 165 | | the cookie from being sent to you if it can not be done securely. 166 | | 167 | */ 168 | 169 | 'secure' => env('SESSION_SECURE_COOKIE', false), 170 | 171 | /* 172 | |-------------------------------------------------------------------------- 173 | | HTTP Access Only 174 | |-------------------------------------------------------------------------- 175 | | 176 | | Setting this value to true will prevent JavaScript from accessing the 177 | | value of the cookie and the cookie will only be accessible through 178 | | the HTTP protocol. You are free to modify this option if needed. 179 | | 180 | */ 181 | 182 | 'http_only' => true, 183 | 184 | /* 185 | |-------------------------------------------------------------------------- 186 | | Same-Site Cookies 187 | |-------------------------------------------------------------------------- 188 | | 189 | | This option determines how your cookies behave when cross-site requests 190 | | take place, and can be used to mitigate CSRF attacks. By default, we 191 | | do not enable this as other CSRF protection services are in place. 192 | | 193 | | Supported: "lax", "strict", "none" 194 | | 195 | */ 196 | 197 | 'same_site' => null, 198 | 199 | ]; 200 | --------------------------------------------------------------------------------