diff --git a/README.md b/README.md index 1770da2..af88e6c 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ git checkout -b dev-package hack hack hack +```bash +build.sh dev-package dev-package/one-app/stub +``` ## Installation diff --git a/composer.json b/composer.json index d0b5ad2..a633a9f 100644 --- a/composer.json +++ b/composer.json @@ -19,9 +19,13 @@ "php": "^8.2", "envor/laravel-datastore": "^1.2", "envor/laravel-schema-macros": "^1.1", - "envor/platform": "^1.3", + "envor/platform": "^1.5", "illuminate/contracts": "^11.0", "inmanturbo/turbohx": "^1.1", + "laravel/jetstream": "^5.0@dev", + "laravel/sanctum": "^4.0@dev", + "laravel/tinker": "^2.9", + "livewire/livewire": "^3.4", "livewire/volt": "^1.6", "spatie/laravel-package-tools": "^1.16.2" }, diff --git a/src/Commands/OneAppCommand.php b/src/Commands/OneAppCommand.php index 4ae838e..a3a38c1 100644 --- a/src/Commands/OneAppCommand.php +++ b/src/Commands/OneAppCommand.php @@ -3,17 +3,61 @@ namespace Envor\OneApp\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\File; class OneAppCommand extends Command { - public $signature = 'one-app'; + public $signature = 'one-app:install'; public $description = 'My command'; public function handle(): int { - $this->comment('All done'); + $this->info('Installing Jetstream...'); - return self::SUCCESS; + $this->callSilent('jetstream:install', [ + 'stack' => 'livewire', + '--teams' => true, + '--dark' => true, + '--api' => true, + '--pest' => true, + '--no-interaction' => true, + ]); + + $this->info('Installing folio...'); + + $this->callSilent('folio:install'); + + $this->info('Installing volt'); + + $this->callSilent('volt:install'); + + $this->info('Publishing Stubs...'); + + $this->copyFiles(); + + $this->info('One App installed'); + + return 0; + } + + protected function copyFiles() + { + $this->info('Copying files...'); + + $sourceDir = realpath(__DIR__.'/../../stubs/'); + $destinationDir = base_path(); + + $files = File::allFiles($sourceDir); + + foreach ($files as $file) { + $destinationFilePath = $destinationDir.'/'.$file->getRelativePathname(); + File::ensureDirectoryExists(dirname($destinationFilePath)); + File::copy($sourceFile = $file->getPathname(), $destinationFilePath); + // check verbosity + if ($this->output->isVerbose()) { + $this->line('Copied '.$sourceFile . ' to ' . $destinationFilePath); + } + } } } diff --git a/stubs/app/Actions/Fortify/CreateNewUser.php b/stubs/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..632bd80 --- /dev/null +++ b/stubs/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,66 @@ + $input + */ + public function create(array $input): User + { + return app(DatabaseManager::class)->usingConnection($this->getConnectionName(), fn () => $this->execute($input)); + } + + /** + * Create a newly registered user. + * + * @param array $input + */ + protected function execute(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => $this->passwordRules(), + 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '', + ])->validate(); + + return DB::transaction(function () use ($input) { + return tap(User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]), function (User $user) { + $this->createTeam($user); + }); + }); + } + + /** + * Create a personal team for the user. + */ + protected function createTeam(User $user): void + { + $user->ownedTeams()->save(Team::forceCreate([ + 'user_id' => $user->id, + 'name' => explode(' ', $user->name, 2)[0]."'s Team", + 'personal_team' => true, + ])); + } +} diff --git a/stubs/app/Actions/Fortify/UpdateUserProfileInformation.php b/stubs/app/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 0000000..7e74869 --- /dev/null +++ b/stubs/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,69 @@ + $input + */ + public function update(User $user, array $input): void + { + app(DatabaseManager::class)->usingConnection($this->getConnectionName(), fn () => $this->execute($user, $input)); + } + + /** + * Validate and update the given user's profile information. + * + * @param array $input + */ + protected function execute(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], + 'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'], + ])->validateWithBag('updateProfileInformation'); + + if (isset($input['photo'])) { + $user->updateProfilePhoto($input['photo']); + } + + if ($input['email'] !== $user->email && + $user instanceof MustVerifyEmail) { + $this->updateVerifiedUser($user, $input); + } else { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + ])->save(); + } + } + + /** + * Update the given verified user's profile information. + * + * @param array $input + */ + protected function updateVerifiedUser(User $user, array $input): void + { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'email_verified_at' => null, + ])->save(); + + $user->sendEmailVerificationNotification(); + } +} diff --git a/stubs/app/Actions/Jetstream/AddTeamMember.php b/stubs/app/Actions/Jetstream/AddTeamMember.php new file mode 100644 index 0000000..43f92a0 --- /dev/null +++ b/stubs/app/Actions/Jetstream/AddTeamMember.php @@ -0,0 +1,81 @@ +authorize('addTeamMember', $team); + + $this->validate($team, $email, $role); + + $newTeamMember = Jetstream::findUserByEmailOrFail($email); + + AddingTeamMember::dispatch($team, $newTeamMember); + + $team->users()->attach( + $newTeamMember, ['role' => $role] + ); + + TeamMemberAdded::dispatch($team, $newTeamMember); + } + + /** + * Validate the add member operation. + */ + protected function validate(Team $team, string $email, ?string $role): void + { + Validator::make([ + 'email' => $email, + 'role' => $role, + ], $this->rules(), [ + 'email.exists' => __('We were unable to find a registered user with this email address.'), + ])->after( + $this->ensureUserIsNotAlreadyOnTeam($team, $email) + )->validateWithBag('addTeamMember'); + } + + /** + * Get the validation rules for adding a team member. + * + * @return array + */ + protected function rules(): array + { + return array_filter([ + 'email' => ['required', 'email', 'exists:users'], + 'role' => Jetstream::hasRoles() + ? ['required', 'string', new Role] + : null, + ]); + } + + /** + * Ensure that the user is not already on the team. + */ + protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure + { + return function ($validator) use ($team, $email) { + $validator->errors()->addIf( + $team->hasUserWithEmail($email), + 'email', + __('This user already belongs to the team.') + ); + }; + } +} diff --git a/stubs/app/Actions/Jetstream/InviteTeamMember.php b/stubs/app/Actions/Jetstream/InviteTeamMember.php new file mode 100644 index 0000000..cc6d192 --- /dev/null +++ b/stubs/app/Actions/Jetstream/InviteTeamMember.php @@ -0,0 +1,100 @@ +usingConnection($this->getConnectionName(), fn () => $this->execute($user, $team, $email, $role)); + } + + /** + * Invite a new team member to the given team. + */ + protected function execute(User $user, Team $team, string $email, ?string $role = null): void + { + Gate::forUser($user)->authorize('addTeamMember', $team); + + $this->validate($team, $email, $role); + + InvitingTeamMember::dispatch($team, $email, $role); + + $invitation = $team->teamInvitations()->create([ + 'email' => $email, + 'role' => $role, + ]); + + Mail::to($email)->send(new TeamInvitation($invitation)); + } + + /** + * Validate the invite member operation. + */ + protected function validate(Team $team, string $email, ?string $role): void + { + Validator::make([ + 'email' => $email, + 'role' => $role, + ], $this->rules($team), [ + 'email.unique' => __('This user has already been invited to the team.'), + ])->after( + $this->ensureUserIsNotAlreadyOnTeam($team, $email) + )->validateWithBag('addTeamMember'); + } + + /** + * Get the validation rules for inviting a team member. + * + * @return array + */ + protected function rules(Team $team): array + { + return array_filter([ + 'email' => [ + 'required', 'email', + Rule::unique('team_invitations')->where(function (Builder $query) use ($team) { + $query->where('team_id', $team->id); + }), + ], + 'role' => Jetstream::hasRoles() + ? ['required', 'string', new Role] + : null, + ]); + } + + /** + * Ensure that the user is not already on the team. + */ + protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure + { + return function ($validator) use ($team, $email) { + $validator->errors()->addIf( + $team->hasUserWithEmail($email), + 'email', + __('This user already belongs to the team.') + ); + }; + } +} diff --git a/stubs/app/Console/Commands/DatastoreRunCommand.php b/stubs/app/Console/Commands/DatastoreRunCommand.php new file mode 100644 index 0000000..be5f0c9 --- /dev/null +++ b/stubs/app/Console/Commands/DatastoreRunCommand.php @@ -0,0 +1,49 @@ +argument('artisanCommand')) { + $question = $this->option('datastore') + ? 'Which artisan command do you want to run for '.$this->option('datastore').'?' + : 'Which artisan command do you want to run for all datastores?'; + $artisanCommand = $this->ask($question); + } + + $artisanCommandCallback = function () use ($artisanCommand) { + $this->info('Running '.$artisanCommand.' on '.config('database.default').'...'); + + return Artisan::call($artisanCommand, [], $this->output); + }; + + if ($databaseName = $this->option('datastore')) { + $datastore = Datastore::where('name', $databaseName)->first(); + + if (! $datastore) { + $this->error('Datastore "'.$databaseName.'" not found.'); + + return 1; + } + + $datastore->database()->run($artisanCommandCallback)->disconnect(); + } else { + Datastore::all()->each(function ($datastore) use ($artisanCommandCallback) { + $datastore->database()->run($artisanCommandCallback)->disconnect(); + }); + } + + return 0; + } +} diff --git a/stubs/app/Models/Membership.php b/stubs/app/Models/Membership.php new file mode 100644 index 0000000..1ffdf34 --- /dev/null +++ b/stubs/app/Models/Membership.php @@ -0,0 +1,18 @@ + + */ + protected $guarded = []; + + /** + * The event map for the model. + * + * @var array + */ + protected $dispatchesEvents = [ + 'created' => TeamCreated::class, + 'updated' => TeamUpdated::class, + 'deleted' => TeamDeleted::class, + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'personal_team' => 'boolean', + ]; + } + + protected function configured(): void + { + app()->forgetInstance('team'); + app()->instance('team', $this); + } +} diff --git a/stubs/app/Models/TeamInvitation.php b/stubs/app/Models/TeamInvitation.php new file mode 100644 index 0000000..812a7b1 --- /dev/null +++ b/stubs/app/Models/TeamInvitation.php @@ -0,0 +1,33 @@ + + */ + protected $fillable = [ + 'email', + 'role', + ]; + + /** + * Get the team that the invitation belongs to. + */ + public function team(): BelongsTo + { + return $this->belongsTo(Jetstream::teamModel()); + } +} diff --git a/stubs/app/Models/User.php b/stubs/app/Models/User.php new file mode 100644 index 0000000..48fb8c8 --- /dev/null +++ b/stubs/app/Models/User.php @@ -0,0 +1,77 @@ + + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + 'two_factor_recovery_codes', + 'two_factor_secret', + ]; + + /** + * The accessors to append to the model's array form. + * + * @var array + */ + protected $appends = [ + 'profile_photo_url', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } +} diff --git a/stubs/app/Providers/ApiServiceProvider.php b/stubs/app/Providers/ApiServiceProvider.php new file mode 100644 index 0000000..817f7f4 --- /dev/null +++ b/stubs/app/Providers/ApiServiceProvider.php @@ -0,0 +1,26 @@ +configureRequests(); + $this->configureQueue(); + } + + public function configureRequests() + { + if (! $this->app->runningInConsole()) { + $domain = $this->app->request->getHost(); + + /** @var \App\Models\Team $team + * query to see if a team owns the current domain + */ + $team = Team::where('domain', $domain)->first(); + + if (isset($team->id) && isset($team->datastore_id)) { + + // migrate only once a day + if (! cache()->has('team_migrated_'.$team->id)) { + $team + ->migrate(); + cache()->put('team_migrated_'.$team->id, true, now()->addDay()); + } + + $team->configure()->use(); + } + } + } + + public function configureQueue() + { + if (isset($this->app['team'])) { + $this->app['queue']->createPayloadUsing(function () { + return $this->app['team'] ? [ + 'team_uuid' => $this->app['team']->uuid, + ] : []; + }); + } + + $this->app['events']->listen(JobProcessing::class, function ($event) { + if (isset($event->job->payload['team_uuid'])) { + $team = Team::whereUuid($event->job->payload['team_uuid'])->first(); + if (isset($team->id)) { + $team->configure()->use(); + } + } + }); + } +} diff --git a/stubs/app/Providers/FolioServiceProvider.php b/stubs/app/Providers/FolioServiceProvider.php new file mode 100644 index 0000000..38b389a --- /dev/null +++ b/stubs/app/Providers/FolioServiceProvider.php @@ -0,0 +1,29 @@ +middleware([ + '*' => [ + // + ], + ]); + } +} diff --git a/stubs/app/Providers/VoltServiceProvider.php b/stubs/app/Providers/VoltServiceProvider.php new file mode 100644 index 0000000..e61d984 --- /dev/null +++ b/stubs/app/Providers/VoltServiceProvider.php @@ -0,0 +1,28 @@ + [ + 'users' => [ + 'provider' => 'users', + 'table' => 'password_reset_tokens', + 'connection' => env('PLATFORM_DB_CONNECTION', 'sqlite'), + 'expire' => 60, + 'throttle' => 60, + ], + ], +]; diff --git a/stubs/config/database.php b/stubs/config/database.php new file mode 100644 index 0000000..828a3dc --- /dev/null +++ b/stubs/config/database.php @@ -0,0 +1,18 @@ + env('PLATFORM_DB_CONNECTION', 'sqlite'), + 'default' => env('DB_CONNECTION', 'sqlite'), +]; diff --git a/stubs/config/platform.php b/stubs/config/platform.php new file mode 100644 index 0000000..2376094 --- /dev/null +++ b/stubs/config/platform.php @@ -0,0 +1,17 @@ + env('LANDING_PAGE_DISK', 'public'), + 'profile_photo_disk' => env('PROFILE_PHOTO_DISK', 'public'), + 'stores_contact_info' => env('STORES_CONTACT_INFO', true), + 'empty_logo_path' => 'profile-photos/no_image.jpg', + 'empty_phone' => '(_ _ _) _ _ _- _ _ _ _', + 'empty_fax' => '(_ _ _) _ _ _- _ _ _ _', + 'logo_path' => env('PLATFORM_LOGO_PATH'), //resource_path('legacy/qwoffice/print/DigLogo.jpg' + 'name' => env('PLATFORM_NAME'), + 'phone' => env('PLATFORM_PHONE_NUMBER'), + 'fax' => env('PLATFORM_FAX_NUMBER'), + 'street_address' => env('PLATFORM_STREET_ADDRESS'), + 'city_state_zip' => env('PLATFORM_CITY_STATE_ZIP'), + 'email' => env('PLATFORM_EMAIL'), +]; diff --git a/stubs/config/session.php b/stubs/config/session.php new file mode 100644 index 0000000..28b46c1 --- /dev/null +++ b/stubs/config/session.php @@ -0,0 +1,5 @@ + env('PLATFORM_DB_CONNECTION'), +]; diff --git a/stubs/database/factories/UserFactory.php b/stubs/database/factories/UserFactory.php new file mode 100644 index 0000000..ea955ad --- /dev/null +++ b/stubs/database/factories/UserFactory.php @@ -0,0 +1,72 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'two_factor_secret' => null, + 'two_factor_recovery_codes' => null, + 'remember_token' => Str::random(10), + 'profile_photo_path' => null, + 'current_team_id' => null, + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } + + /** + * Indicate that the user should have a personal team. + */ + public function withPersonalTeam(?callable $callback = null): static + { + if (! Features::hasTeamFeatures()) { + return $this->state([]); + } + + return $this->has( + Team::factory() + ->state(fn (array $attributes, User $user) => [ + 'name' => $user->name.'\'s Team', + 'user_id' => $user->id, + 'personal_team' => true, + ]) + ->when(is_callable($callback), $callback), + 'ownedTeams' + ); + } +} diff --git a/stubs/database/migrations/platform/0001_01_01_000000_create_users_table.php b/stubs/database/migrations/platform/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..e8c5b04 --- /dev/null +++ b/stubs/database/migrations/platform/0001_01_01_000000_create_users_table.php @@ -0,0 +1,52 @@ +id(); + $table->uuid('uuid')->index()->unique(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->foreignId('current_team_id')->nullable(); + $table->string('profile_photo_path', 2048)->nullable(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/stubs/database/migrations/platform/0001_01_01_000001_create_cache_table.php b/stubs/database/migrations/platform/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..b9c106b --- /dev/null +++ b/stubs/database/migrations/platform/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/stubs/database/migrations/platform/0001_01_01_000002_create_jobs_table.php b/stubs/database/migrations/platform/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/stubs/database/migrations/platform/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/stubs/database/migrations/platform/2024_02_22_174016_add_two_factor_columns_to_users_table.php b/stubs/database/migrations/platform/2024_02_22_174016_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..b490e24 --- /dev/null +++ b/stubs/database/migrations/platform/2024_02_22_174016_add_two_factor_columns_to_users_table.php @@ -0,0 +1,46 @@ +text('two_factor_secret') + ->after('password') + ->nullable(); + + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + + if (Fortify::confirmsTwoFactorAuthentication()) { + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(array_merge([ + 'two_factor_secret', + 'two_factor_recovery_codes', + ], Fortify::confirmsTwoFactorAuthentication() ? [ + 'two_factor_confirmed_at', + ] : [])); + }); + } +}; diff --git a/stubs/database/migrations/platform/2024_02_22_174023_create_personal_access_tokens_table.php b/stubs/database/migrations/platform/2024_02_22_174023_create_personal_access_tokens_table.php new file mode 100644 index 0000000..e828ad8 --- /dev/null +++ b/stubs/database/migrations/platform/2024_02_22_174023_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/stubs/database/migrations/platform/2024_02_22_174023_create_teams_table.php b/stubs/database/migrations/platform/2024_02_22_174023_create_teams_table.php new file mode 100644 index 0000000..39f78e1 --- /dev/null +++ b/stubs/database/migrations/platform/2024_02_22_174023_create_teams_table.php @@ -0,0 +1,35 @@ +id(); + $table->uuid('uuid')->index()->unique(); + $table->foreignId('user_id')->index(); + $table->string('name'); + $table->string('domain')->nullable()->index()->unique(); + $table->boolean('personal_team'); + $table->text('profile_photo_path')->nullable(); + $table->foreignId('datastore_id')->nullable()->index(); + $table->text('contact_data')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('teams'); + } +}; diff --git a/stubs/database/migrations/platform/2024_02_22_174024_create_team_user_table.php b/stubs/database/migrations/platform/2024_02_22_174024_create_team_user_table.php new file mode 100644 index 0000000..3e27876 --- /dev/null +++ b/stubs/database/migrations/platform/2024_02_22_174024_create_team_user_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('team_id'); + $table->foreignId('user_id'); + $table->string('role')->nullable(); + $table->timestamps(); + + $table->unique(['team_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('team_user'); + } +}; diff --git a/stubs/database/migrations/platform/2024_02_22_174025_create_team_invitations_table.php b/stubs/database/migrations/platform/2024_02_22_174025_create_team_invitations_table.php new file mode 100644 index 0000000..5fe558f --- /dev/null +++ b/stubs/database/migrations/platform/2024_02_22_174025_create_team_invitations_table.php @@ -0,0 +1,33 @@ +id(); + $table->uuid('uuid')->index()->unique(); + $table->foreignId('team_id')->constrained()->cascadeOnDelete(); + $table->string('email'); + $table->string('role')->nullable(); + $table->timestamps(); + + $table->unique(['team_id', 'email']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('team_invitations'); + } +}; diff --git a/stubs/database/migrations/platform/2024_02_22_212041_create_datastores_table.php b/stubs/database/migrations/platform/2024_02_22_212041_create_datastores_table.php new file mode 100644 index 0000000..64cc18f --- /dev/null +++ b/stubs/database/migrations/platform/2024_02_22_212041_create_datastores_table.php @@ -0,0 +1,25 @@ +id(); + $table->uuid('uuid')->index()->unique(); + $table->string('name')->index()->unique(); + $table->string('driver'); + $table->nullableMorphs('owner'); + $table->timestamps(); + }); + } +}; diff --git a/stubs/database/migrations/platform/2024_02_22_212418_create_landing_pages_table.php b/stubs/database/migrations/platform/2024_02_22_212418_create_landing_pages_table.php new file mode 100644 index 0000000..ace45d4 --- /dev/null +++ b/stubs/database/migrations/platform/2024_02_22_212418_create_landing_pages_table.php @@ -0,0 +1,25 @@ +id(); + + $table->nullableMorphs('model'); + + $table->uuid('uuid')->unique(); + + $table->string('name')->nullable(); + + $table->string('landing_page_path', 2048)->nullable(); + + $table->timestamps(); + }); + } +}; diff --git a/stubs/resources/views/livewire/.gitkeep b/stubs/resources/views/livewire/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/stubs/resources/views/pages/.gitkeep b/stubs/resources/views/pages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/stubs/resources/views/teams/show.blade.php b/stubs/resources/views/teams/show.blade.php new file mode 100644 index 0000000..2f316c7 --- /dev/null +++ b/stubs/resources/views/teams/show.blade.php @@ -0,0 +1,46 @@ + + +

+ {{ __('Team Settings') }} +

+
+ +
+
+ @if (Gate::check('update', $team)) + @livewire('update-belongs-to-datastore-form', ['model' => $team]) + + @endif + + @livewire('teams.update-team-name-form', ['team' => $team]) + + + @livewire('update-contact-info-form', ['model' => $team, 'readonly' => ! Gate::check('update', $team)]) + + @if (Gate::check('update', $team)) + + @livewire('update-logo-form', ['model' => $team]) + @endif + + @if (Gate::check('update', $team)) + + @livewire('update-landing-page-form', ['model' => $team]) + @endif + + @if (Gate::check('update', $team)) + + @livewire('update-model-domain-form', ['model' => $team, 'readonly' => ! Gate::check('update', $team)]) + @endif + + @livewire('teams.team-member-manager', ['team' => $team]) + + @if (Gate::check('delete', $team) && ! $team->personal_team) + + +
+ @livewire('teams.delete-team-form', ['team' => $team]) +
+ @endif +
+
+
diff --git a/stubs/routes/web.php b/stubs/routes/web.php new file mode 100644 index 0000000..8849622 --- /dev/null +++ b/stubs/routes/web.php @@ -0,0 +1,24 @@ +landingPage) { + return response()->file(Storage::disk($team->landingPageDisk())->path($team->landingPagePath())); + } + + return view('welcome'); +}); + +Route::middleware([ + 'auth:sanctum', + config('jetstream.auth_session'), + 'verified', +])->group(function () { + Route::get('/dashboard', function () { + return view('dashboard'); + })->name('dashboard'); +}); diff --git a/stubs/tests/Pest.php b/stubs/tests/Pest.php new file mode 100644 index 0000000..fd03438 --- /dev/null +++ b/stubs/tests/Pest.php @@ -0,0 +1,66 @@ +beforeEach(function () { + + Datastore::fake(); + + config(['database.platform' => 'testing_platform']); + config(['database.default' => 'testing_app']); + + config(['database.connections.sqlite.database' => ':memory:']); + + config(['database.connections.testing_platform' => config('database.connections.sqlite')]); + + config(['database.connections.testing_app' => config('database.connections.sqlite')]); + + Artisan::call('migrate:fresh --database=testing_platform --path=database/migrations/platform'); + }) + ->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +}