diff --git a/app/Concerns/MustSetInitialPassword.php b/app/Concerns/MustSetInitialPassword.php new file mode 100644 index 00000000..c8763794 --- /dev/null +++ b/app/Concerns/MustSetInitialPassword.php @@ -0,0 +1,48 @@ +password) { + $user->password = Hash::make(Str::random(128)); + } + }); + + static::created(function (self $user) { + $user->sendWelcomeNotification(); + }); + } + + public function hasSetPassword(): bool + { + return ! \is_null($this->password_set_at); + } + + public function setPassword(string $password): bool + { + return $this->update([ + 'password' => Hash::make($password), + 'password_set_at' => $this->freshTimestamp(), + ]); + } + + public function sendWelcomeNotification(): void + { + $this->notify( + $this->is_admin + ? new AdminWelcomeNotification + : new WelcomeNotification + ); + } +} diff --git a/app/Filament/Admin/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php b/app/Filament/Admin/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php index 3afd0343..56b3823b 100644 --- a/app/Filament/Admin/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php +++ b/app/Filament/Admin/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php @@ -29,12 +29,21 @@ public function table(Table $table): Table { return $table ->columns([ - TextColumn::make('id'), - TextColumn::make('first_name'), - TextColumn::make('last_name'), - TextColumn::make('email'), - TextColumn::make('role'), - TextColumn::make('created_at'), + TextColumn::make('id') + ->label(__('field.id')) + ->shrink(), + + TextColumn::make('first_name') + ->label(__('field.first_name')), + + TextColumn::make('last_name') + ->label(__('field.last_name')), + + TextColumn::make('email') + ->label(__('field.email')), + + TextColumn::make('last_login_at') + ->label(__('field.last_login_at')), ]) ->filters([ // diff --git a/app/Filament/Admin/Resources/UserResource.php b/app/Filament/Admin/Resources/UserResource.php index 14f15021..2e885708 100644 --- a/app/Filament/Admin/Resources/UserResource.php +++ b/app/Filament/Admin/Resources/UserResource.php @@ -6,11 +6,17 @@ use App\Filament\Admin\Resources\UserResource\Pages; use App\Models\User; +use Filament\Forms\Components\Radio; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TextInput; use Filament\Forms\Form; +use Filament\Forms\Get; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; +use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Table; class UserResource extends Resource @@ -36,8 +42,53 @@ public static function getPluralModelLabel(): string public static function form(Form $form): Form { return $form + ->inlineLabel() ->schema([ - // + Section::make() + ->maxWidth('3xl') + ->schema([ + TextInput::make('first_name') + ->label(__('field.first_name')) + ->maxLength(100) + ->required(), + + TextInput::make('last_name') + ->label(__('field.last_name')) + ->maxLength(100) + ->required(), + + TextInput::make('email') + ->label(__('field.email')) + ->unique(ignoreRecord: true) + ->columnSpanFull() + ->maxLength(200) + ->email() + ->required(), + ]), + + Section::make() + ->maxWidth('3xl') + // ->columns() + ->schema([ + Radio::make('is_admin') + ->label(__('field.role')) + ->inlineLabel() + ->boolean( + trueLabel: __('user.role.admin'), + falseLabel: __('user.role.user'), + ) + ->default(false) + ->live(), + + Select::make('organizations') + ->relationship('organizations', titleAttribute: 'name') + ->label(__('field.organizations')) + ->inlineLabel() + ->visible(fn (Get $get) => \boolval($get('is_admin')) === false) + ->multiple() + ->preload() + ->required(), + ]), ]); } @@ -51,15 +102,23 @@ public static function table(Table $table): Table TextColumn::make('last_name') ->searchable(), - TextColumn::make('organizations.name'), + TextColumn::make('organizations.name') + ->wrap(), - TextColumn::make('roles'), + TextColumn::make('is_admin') + ->label(__('field.role')), TextColumn::make('account_status'), - TextColumn::make('last_login_at'), + TextColumn::make('last_login_at') + ->sortable(), ]) ->filters([ + TernaryFilter::make('is_admin') + ->label(__('field.role')) + ->trueLabel(__('user.role.admin')) + ->falseLabel(__('user.role.user')), + SelectFilter::make('organizations') ->relationship('organizations', 'name') ->multiple(), diff --git a/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php index 330d3996..d69f1e44 100644 --- a/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php +++ b/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php @@ -1,12 +1,15 @@ count() ), + + Stat::make( + __('user.stats.total'), + User::query() + ->count() + ), ]; } } diff --git a/app/Filament/Organizations/Resources/BeneficiaryResource.php b/app/Filament/Organizations/Resources/BeneficiaryResource.php index 297fd977..650b179b 100644 --- a/app/Filament/Organizations/Resources/BeneficiaryResource.php +++ b/app/Filament/Organizations/Resources/BeneficiaryResource.php @@ -54,9 +54,7 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('id') ->label(__('field.case_id')) - ->extraHeaderAttributes([ - 'class' => 'w-1', - ]) + ->shrink() ->sortable() ->searchable(), @@ -67,25 +65,19 @@ public static function table(Table $table): Table TextColumn::make('created_at') ->label(__('field.open_at')) ->date() - ->extraHeaderAttributes([ - 'class' => 'w-1', - ]) + ->shrink() ->sortable(), TextColumn::make('last_evaluated_at') ->label(__('field.last_evaluated_at')) ->date() - ->extraHeaderAttributes([ - 'class' => 'w-1', - ]) + ->shrink() ->sortable(), TextColumn::make('last_serviced_at') ->label(__('field.last_serviced_at')) ->date() - ->extraHeaderAttributes([ - 'class' => 'w-1', - ]) + ->shrink() ->sortable(), TextColumn::make('status') @@ -99,9 +91,7 @@ public static function table(Table $table): Table default => dd($state) }) ->formatStateUsing(fn ($state) => $state?->label()) - ->extraHeaderAttributes([ - 'class' => 'w-1', - ]), + ->shrink(), ]) ->filters([ // diff --git a/app/Http/Middleware/UpdateDefaultTenant.php b/app/Http/Middleware/UpdateDefaultTenant.php new file mode 100644 index 00000000..136a3cd7 --- /dev/null +++ b/app/Http/Middleware/UpdateDefaultTenant.php @@ -0,0 +1,32 @@ +user(); + $tenant = Filament::getTenant(); + + if ($user->latest_organization_id !== $tenant->id) { + $user->update([ + 'latest_organization_id' => $tenant->id, + ]); + } + + return $next($request); + } +} diff --git a/app/Http/Responses/LoginResponse.php b/app/Http/Responses/LoginResponse.php new file mode 100644 index 00000000..ba8f4892 --- /dev/null +++ b/app/Http/Responses/LoginResponse.php @@ -0,0 +1,28 @@ +user(); + + if ($user->is_admin) { + $panel = Filament::getPanel('admin'); + $parameters = []; + } else { + $panel = Filament::getPanel('organization'); + $parameters = $user->getDefaultTenant($panel); + } + + return redirect()->intended($panel->route('pages.dashboard', $parameters)); + } +} diff --git a/app/Listeners/LogSuccessfulLogin.php b/app/Listeners/LogSuccessfulLogin.php new file mode 100644 index 00000000..ac5fccb1 --- /dev/null +++ b/app/Listeners/LogSuccessfulLogin.php @@ -0,0 +1,23 @@ +by($event->user) + ->on($event->user) + ->withProperties([ + 'ip' => request()->ip(), + 'user_agent' => request()->userAgent(), + ]) + ->event('logged_in') + ->log('logged_in'); + } +} diff --git a/app/Livewire/Welcome.php b/app/Livewire/Welcome.php new file mode 100644 index 00000000..fab1a2db --- /dev/null +++ b/app/Livewire/Welcome.php @@ -0,0 +1,111 @@ +check()) { + redirect()->intended(Filament::getUrl()); + } + + if (! $request->hasValidSignature()) { + abort(Response::HTTP_FORBIDDEN, __('auth.invalid_signature')); + } + + if (\is_null($this->user)) { + abort(Response::HTTP_FORBIDDEN, __('auth.invalid_user')); + } + + if ($this->user->hasSetPassword()) { + abort(Response::HTTP_FORBIDDEN, __('auth.link_already_used')); + } + + $this->form->fill([ + 'email' => $this->user->email, + ]); + } + + public function handle(): ?LoginResponse + { + try { + $this->rateLimit(5); + } catch (TooManyRequestsException $exception) { + throw ValidationException::withMessages([ + 'email' => __('filament::login.messages.throttled', [ + 'seconds' => $exception->secondsUntilAvailable, + 'minutes' => ceil($exception->secondsUntilAvailable / 60), + ]), + ]); + } + + $this->user->setPassword( + data_get($this->form->getState(), 'password') + ); + + Filament::auth()->login($this->user); + + return app(LoginResponse::class); + } + + public function form(Form $form): Form + { + return $form + ->schema([ + TextInput::make('email') + ->label(__('filament-panels::pages/auth/register.form.email.label')) + ->email() + ->disabled(), + + TextInput::make('password') + ->label(__('filament-panels::pages/auth/register.form.password.label')) + ->password() + ->required() + ->confirmed(), + + TextInput::make('password_confirmation') + ->label(__('filament-panels::pages/auth/register.form.password_confirmation.label')) + ->password() + ->required(), + ]) + ->statePath('data'); + } + + protected function getFormActions(): array + { + return [ + Action::make('handle') + ->label(__('auth.set_password')) + ->submit('handle'), + ]; + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php new file mode 100644 index 00000000..1890fbfe --- /dev/null +++ b/app/Models/Activity.php @@ -0,0 +1,45 @@ +latest(); + }); + } + + public function scopeBetweenDates(Builder $query, ?string $from = null, ?string $until = null): Builder + { + return $query + ->when($from, function (Builder $query, string $date) { + $query->whereDate('created_at', '>=', $date); + }) + ->when($until, function (Builder $query, string $date) { + $query->whereDate('created_at', '<=', $date); + }); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 66400c5f..f76c0e97 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,32 +4,40 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; - use App\Concerns\HasUlid; +use App\Concerns\MustSetInitialPassword; use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\HasAvatar; +use Filament\Models\Contracts\HasDefaultTenant; use Filament\Models\Contracts\HasName; use Filament\Models\Contracts\HasTenants; use Filament\Panel; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; use Jeffgreco13\FilamentBreezy\Traits\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; +use Spatie\Activitylog\LogOptions; +use Spatie\Activitylog\Traits\CausesActivity; +use Spatie\Activitylog\Traits\LogsActivity; use Spatie\Image\Enums\Fit; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; -class User extends Authenticatable implements FilamentUser, HasAvatar, HasName, HasMedia, HasTenants +class User extends Authenticatable implements FilamentUser, HasAvatar, HasName, HasMedia, HasTenants, HasDefaultTenant { + use CausesActivity; use HasApiTokens; use HasFactory; use HasUlid; use InteractsWithMedia; + use LogsActivity; + use MustSetInitialPassword; use Notifiable; use TwoFactorAuthenticatable; @@ -43,6 +51,9 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasName, 'last_name', 'email', 'password', + 'password_set_at', + 'latest_organization_id', + 'is_admin', ]; /** @@ -61,16 +72,36 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasName, * @var array */ protected $casts = [ - 'email_verified_at' => 'datetime', + 'password_set_at' => 'datetime', 'password' => 'hashed', 'is_admin' => 'boolean', ]; + protected static function booted() + { + static::addGlobalScope('withLastLogin', function (Builder $query) { + return $query->withLastLoginAt(); + }); + } + public function organizations(): MorphToMany { return $this->morphToMany(Organization::class, 'model', 'model_has_organizations', 'model_id'); } + public function latestOrganization(): BelongsTo + { + return $this->belongsTo(Organization::class, 'latest_organization_id'); + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->logExcept($this->hidden) + ->logOnlyDirty(); + } + public function registerMediaCollections(): void { $this->addMediaCollection('avatar') @@ -120,4 +151,25 @@ public function canAccessTenant(Model $tenant): bool { return $this->organizations->contains($tenant); } + + public function getDefaultTenant(Panel $panel): ?Model + { + return $this->latestOrganization ?? $this->getTenants($panel)->first(); + } + + public function scopeWithLastLoginAt(Builder $query): Builder + { + return $query + ->addSelect([ + 'last_login_at' => Activity::query() + ->select('created_at') + ->where('subject_type', $this->getMorphClass()) + ->whereColumn('subject_id', 'users.id') + ->where('log_name', 'system') + ->where('event', 'logged_in') + ->latest() + ->take(1), + ]) + ->withCasts(['last_login_at' => 'datetime']); + } } diff --git a/app/Notifications/Admin/WelcomeNotification.php b/app/Notifications/Admin/WelcomeNotification.php new file mode 100644 index 00000000..cf0228fe --- /dev/null +++ b/app/Notifications/Admin/WelcomeNotification.php @@ -0,0 +1,42 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject(__('email.admin.welcome.subject')) + ->line(__('email.admin.welcome.intro_line_1')) + ->line(__('email.admin.welcome.intro_line_2')) + ->action( + __('email.admin.welcome.accept_invitation'), + URL::signedRoute($this->route, ['user' => $notifiable]) + ); + } +} diff --git a/app/Notifications/Organizations/WelcomeNotification.php b/app/Notifications/Organizations/WelcomeNotification.php new file mode 100644 index 00000000..ae8f54f3 --- /dev/null +++ b/app/Notifications/Organizations/WelcomeNotification.php @@ -0,0 +1,42 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject(__('email.organization.welcome.subject')) + ->line(__('email.organization.welcome.intro_line_1')) + ->line(__('email.organization.welcome.intro_line_2')) + ->action( + __('email.organization.welcome.accept_invitation'), + URL::signedRoute($this->route, ['user' => $notifiable]) + ); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index e18de24a..0cbc8e61 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; +use App\Http\Responses\LoginResponse; use App\Models\Beneficiary; use App\Models\City; use App\Models\CommunityProfile; @@ -13,6 +14,8 @@ use App\Models\ReferringInstitution; use App\Models\Service; use App\Models\User; +use Filament\Http\Responses\Auth\Contracts\LoginResponse as LoginResponseContract; +use Filament\Tables\Columns\Column; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Schema\Blueprint; @@ -28,6 +31,10 @@ public function register(): void { $this->registerBlueprintMacros(); $this->registerViteMacros(); + + $this->app->bind(LoginResponseContract::class, LoginResponse::class); + + Column::macro('shrink', fn () => $this->extraHeaderAttributes(['class' => 'w-1'])); } /** diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index ee09f108..7cbef06c 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -35,6 +35,6 @@ public function boot(): void */ public function shouldDiscoverEvents(): bool { - return false; + return true; } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 6f4e5049..f8f99af8 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -5,6 +5,7 @@ namespace App\Providers\Filament; use App\Filament\Admin\Pages; +use App\Livewire\Welcome; use Filament\Forms\Components\DateTimePicker; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -22,6 +23,7 @@ use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; +use Illuminate\Support\Facades\Route; use Illuminate\View\Middleware\ShareErrorsFromSession; use Jeffgreco13\FilamentBreezy\BreezyCore; @@ -70,6 +72,9 @@ public function panel(Panel $panel): Panel ->pages([ Pages\Dashboard::class, ]) + ->routes(function () { + Route::get('/welcome/{user:ulid}', Welcome::class)->name('auth.welcome'); + }) ->discoverWidgets( in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets' diff --git a/app/Providers/Filament/OrganizationPanelProvider.php b/app/Providers/Filament/OrganizationPanelProvider.php index 50a0efa8..e943ba44 100644 --- a/app/Providers/Filament/OrganizationPanelProvider.php +++ b/app/Providers/Filament/OrganizationPanelProvider.php @@ -6,6 +6,8 @@ use App\Filament\Organizations\Pages; use App\Filament\Organizations\Pages\Profile\UserPersonalInfo; +use App\Http\Middleware\UpdateDefaultTenant; +use App\Livewire\Welcome; use App\Models\Organization; use Filament\Facades\Filament; use Filament\Forms\Components\DateTimePicker; @@ -26,6 +28,7 @@ use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; +use Illuminate\Support\Facades\Route; use Illuminate\View\Middleware\ShareErrorsFromSession; use Jeffgreco13\FilamentBreezy\BreezyCore; use Livewire\Livewire; @@ -80,6 +83,9 @@ public function panel(Panel $panel): Panel ->pages([ Pages\Dashboard::class, ]) + ->routes(function () { + Route::get('/welcome/{user:ulid}', Welcome::class)->name('auth.welcome'); + }) ->discoverWidgets( in: app_path('Filament/Organizations/Widgets'), for: 'App\\Filament\\Organizations\\Widgets' @@ -128,7 +134,10 @@ public function panel(Panel $panel): Panel ]) ->tenant(Organization::class, 'slug') ->tenantProfile(Pages\Tenancy\EditOrganizationProfile::class) - ->tenantRoutePrefix('org'); + ->tenantRoutePrefix('org') + ->tenantMiddleware([ + UpdateDefaultTenant::class, + ]); } protected function setDefaultDateTimeDisplayFormats(): void diff --git a/composer.json b/composer.json index beeae967..ddbff153 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "laravel/sanctum": "^3.3", "laravel/tinker": "^2.8", "sentry/sentry-laravel": "^4.1", + "spatie/laravel-activitylog": "^4.7", "stevegrunwell/time-constants": "^1.2", "tpetry/laravel-query-expressions": "^0.9" }, diff --git a/composer.lock b/composer.lock index 79993c1e..cddfbd1d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9eae82c64a885b1b74f2e8ca7563679b", + "content-hash": "f56c2aed012e711a118f78f8e8289f47", "packages": [ { "name": "alcea/cnp", @@ -5908,6 +5908,97 @@ ], "time": "2023-07-19T18:55:36+00:00" }, + { + "name": "spatie/laravel-activitylog", + "version": "4.7.3", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "ec65a478a909b8df1b4f0c3c45de2592ca7639e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/ec65a478a909b8df1b4f0c3c45de2592ca7639e5", + "reference": "ec65a478a909b8df1b4f0c3c45de2592ca7639e5", + "shasum": "" + }, + "require": { + "illuminate/config": "^8.0 || ^9.0 || ^10.0", + "illuminate/database": "^8.69 || ^9.27 || ^10.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0", + "php": "^8.0", + "spatie/laravel-package-tools": "^1.6.3" + }, + "require-dev": { + "ext-json": "*", + "orchestra/testbench": "^6.23 || ^7.0 || ^8.0", + "pestphp/pest": "^1.20" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Activitylog\\ActivitylogServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Activitylog\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev.gummibeer@gmail.com", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "A very simple activity logger to monitor the users of your website or application", + "homepage": "https://github.com/spatie/activitylog", + "keywords": [ + "activity", + "laravel", + "log", + "spatie", + "user" + ], + "support": { + "issues": "https://github.com/spatie/laravel-activitylog/issues", + "source": "https://github.com/spatie/laravel-activitylog/tree/4.7.3" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2023-01-25T17:04:51+00:00" + }, { "name": "spatie/laravel-medialibrary", "version": "11.0.1", diff --git a/config/activitylog.php b/config/activitylog.php new file mode 100644 index 00000000..a4321ae6 --- /dev/null +++ b/config/activitylog.php @@ -0,0 +1,54 @@ + env('ACTIVITY_LOGGER_ENABLED', true), + + /* + * When the clean-command is executed, all recording activities older than + * the number of days specified here will be deleted. + */ + 'delete_records_older_than_days' => PHP_INT_MAX, + + /* + * If no log name is passed to the activity() helper + * we use this default log name. + */ + 'default_log_name' => 'default', + + /* + * You can specify an auth driver here that gets user models. + * If this is null we'll use the current Laravel auth driver. + */ + 'default_auth_driver' => null, + + /* + * If set to true, the subject returns soft deleted models. + */ + 'subject_returns_soft_deleted_models' => false, + + /* + * This model will be used to log activity. + * It should implement the Spatie\Activitylog\Contracts\Activity interface + * and extend Illuminate\Database\Eloquent\Model. + */ + 'activity_model' => \App\Models\Activity::class, + + /* + * This is the name of the table that will be created by the migration and + * used by the Activity model shipped with this package. + */ + 'table_name' => 'activity_log', + + /* + * This is the database connection that will be used by the migration and + * the Activity model shipped with this package. In case it's not set + * Laravel's database.default will be used instead. + */ + 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'), +]; diff --git a/database/factories/OrganizationFactory.php b/database/factories/OrganizationFactory.php index 4beba1bb..05b72a2d 100644 --- a/database/factories/OrganizationFactory.php +++ b/database/factories/OrganizationFactory.php @@ -46,7 +46,7 @@ public function definition(): array ]; } - public function withUsers(int $count = 25): static + public function withUsers(int $count = 5): static { return $this->afterCreating(function (Organization $organization) use ($count) { $organization->users()->attach( diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 86b9ad2a..e659559b 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -28,7 +28,6 @@ public function definition(): array 'first_name' => fake()->firstName(), 'last_name' => fake()->lastName(), 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), ]; diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index 28b68e3f..1e265248 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -16,8 +16,8 @@ public function up(): void $table->string('first_name'); $table->string('last_name'); $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); $table->boolean('is_admin')->default(false); + $table->timestamp('password_set_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); diff --git a/database/migrations/2023_12_22_104920_add_last_organization_id_column_to_users_table.php b/database/migrations/2023_12_22_104920_add_last_organization_id_column_to_users_table.php new file mode 100644 index 00000000..68b34c83 --- /dev/null +++ b/database/migrations/2023_12_22_104920_add_last_organization_id_column_to_users_table.php @@ -0,0 +1,24 @@ +foreignIdFor(Organization::class, 'latest_organization_id') + ->nullable() + ->constrained('organizations') + ->cascadeOnDelete(); + }); + } +}; diff --git a/database/migrations/2023_12_22_111436_create_activity_log_table.php b/database/migrations/2023_12_22_111436_create_activity_log_table.php new file mode 100644 index 00000000..e943db15 --- /dev/null +++ b/database/migrations/2023_12_22_111436_create_activity_log_table.php @@ -0,0 +1,27 @@ +create(config('activitylog.table_name'), function (Blueprint $table) { + $table->id(); + $table->string('log_name')->nullable()->index(); + $table->text('description'); + $table->nullableMorphs('subject', 'subject'); + $table->nullableMorphs('causer', 'causer'); + $table->json('properties')->nullable(); + $table->timestamp('created_at'); + }); + } +}; diff --git a/database/migrations/2023_12_22_111437_add_event_column_to_activity_log_table.php b/database/migrations/2023_12_22_111437_add_event_column_to_activity_log_table.php new file mode 100644 index 00000000..3116704a --- /dev/null +++ b/database/migrations/2023_12_22_111437_add_event_column_to_activity_log_table.php @@ -0,0 +1,21 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->string('event')->nullable()->after('subject_type'); + }); + } +}; diff --git a/database/migrations/2023_12_22_111438_add_batch_uuid_column_to_activity_log_table.php b/database/migrations/2023_12_22_111438_add_batch_uuid_column_to_activity_log_table.php new file mode 100644 index 00000000..45936c90 --- /dev/null +++ b/database/migrations/2023_12_22_111438_add_batch_uuid_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->uuid('batch_uuid')->nullable()->after('properties'); + }); + } +} +; diff --git a/lang/ro/auth.php b/lang/ro/auth.php index 986547b3..449ca398 100644 --- a/lang/ro/auth.php +++ b/lang/ro/auth.php @@ -19,4 +19,9 @@ 'password' => 'Parola introdusă este greșită.', 'throttle' => 'Prea multe încercări de intrare în cont. Puteți încerca din nou peste :seconds secunde.', + 'link_already_used' => 'Acest link a fost folosit deja.', + 'invalid_signature' => 'Acest link nu are o semnătură validă.', + 'invalid_user' => 'Acest utilizator nu este valid.', + 'set_password' => 'Setează parola', + ]; diff --git a/lang/ro/email.php b/lang/ro/email.php new file mode 100644 index 00000000..d611e3a8 --- /dev/null +++ b/lang/ro/email.php @@ -0,0 +1,24 @@ + [ + 'welcome' => [ + 'intro_line_1' => 'Ești invitat să te alături platformei Sunrise ca administrator.', + 'intro_line_2' => 'Acceptă invitația pentru a-ți putea accesa contul.', + 'subject' => 'Invitație la platforma Sunrise', + 'accept_invitation' => 'Acceptă invitația', + ], + ], + + 'organization' => [ + 'welcome' => [ + 'intro_line_1' => 'Ești invitat să te alături platformei Sunrise.', + 'intro_line_2' => 'Acceptă invitația pentru a-ți putea accesa contul.', + 'subject' => 'Invitație la platforma Sunrise', + 'accept_invitation' => 'Acceptă invitația', + ], + ], +]; diff --git a/lang/ro/field.php b/lang/ro/field.php index 6f335eab..1afdc723 100644 --- a/lang/ro/field.php +++ b/lang/ro/field.php @@ -49,6 +49,8 @@ 'nurse' => 'Asistent Medical Comunitar', 'open_at' => 'Deschis la', 'organizer' => 'Organizator', + 'organization' => 'Organizație', + 'organizations' => 'Organizații', 'outside_working_hours' => 'Realizat în afara programului de lucru', 'participants_list' => 'Listă nominală participanți', 'participants' => 'Participanți', @@ -126,5 +128,8 @@ 'act_location_other' => 'Specificați ce alt loc', 'first_called_institution' => 'Prima instituție la care a apelat victima', 'other_called_institutions' => 'Următoarele instituții la care a apelat victima', + 'last_login_at' => 'Ultima accesare', + + 'role' => 'Rol', ]; diff --git a/lang/ro/user.php b/lang/ro/user.php index ace48901..d68d4453 100644 --- a/lang/ro/user.php +++ b/lang/ro/user.php @@ -19,4 +19,11 @@ 'monitoring' => 'Cazuri în monitorizare', 'closed' => 'Cazuri închise', ], + + 'role' => [ + 'admin' => 'Administrator', + 'specialist' => 'Specialist', + 'manager' => 'Manager', + + ], ]; diff --git a/resources/views/livewire/welcome.blade.php b/resources/views/livewire/welcome.blade.php new file mode 100644 index 00000000..40bedd21 --- /dev/null +++ b/resources/views/livewire/welcome.blade.php @@ -0,0 +1,10 @@ + + + {{ $this->form }} + + + +