mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 16:22:16 +01:00
Compare commits
5 Commits
feature/de
...
feature/ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2cdbe1e7e | ||
|
|
9158b445f1 | ||
|
|
3d58f570bd | ||
|
|
400bc434b9 | ||
|
|
2ab28001be |
@@ -26,7 +26,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
/**
|
||||
* Create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
* @param array<string, mixed> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace App\Actions\Fortify;
|
||||
|
||||
use App\Enums\Weekday;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -59,8 +58,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
$user->updateProfilePhoto($input['photo']);
|
||||
}
|
||||
|
||||
if ($input['email'] !== $user->email &&
|
||||
$user instanceof MustVerifyEmail) {
|
||||
if ($input['email'] !== $user->email) {
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
|
||||
@@ -57,7 +57,7 @@ class AddOrganizationMember implements AddsTeamMembers
|
||||
*/
|
||||
protected function rules(): array
|
||||
{
|
||||
return array_filter([
|
||||
return [
|
||||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
@@ -75,7 +75,7 @@ class AddOrganizationMember implements AddsTeamMembers
|
||||
Role::Employee->value,
|
||||
]),
|
||||
],
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Auth;
|
||||
|
||||
use App\Mail\AuthApiTokenExpirationReminderMail;
|
||||
use App\Mail\AuthApiTokenExpiredMail;
|
||||
use App\Models\Passport\Token;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class AuthSendReminderForExpiringApiTokensCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'auth:send-mails-expiring-api-tokens '.
|
||||
' { --dry-run : Do not actually send emails or save anything to the database, just output what would happen }';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Sends emails about expiring API tokens, one week before and when they expired.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
if ($dryRun) {
|
||||
$this->comment('Running in dry-run mode. No emails will be sent and nothing will be saved to the database.');
|
||||
}
|
||||
|
||||
$this->comment('Sending reminder emails about expiring API tokens...');
|
||||
$sentMails = 0;
|
||||
Token::query()
|
||||
->where('expires_at', '<=', Carbon::now()->addDays(7))
|
||||
->whereNull('reminder_sent_at')
|
||||
->with([
|
||||
'client',
|
||||
'user',
|
||||
])
|
||||
->whereHas('user', function (Builder $query): void {
|
||||
/** @var Builder<User> $query */
|
||||
$query->where('is_placeholder', '=', false);
|
||||
})
|
||||
->isApiToken(true)
|
||||
->orderBy('created_at', 'asc')
|
||||
->chunk(500, function (Collection $tokens) use ($dryRun, &$sentMails): void {
|
||||
/** @var Collection<int, Token> $tokens */
|
||||
foreach ($tokens as $token) {
|
||||
$user = $token->user;
|
||||
$this->info('Start sending email to user "'.$user->email.'" ('.$user->getKey().') reminding about API token '.$token->getKey());
|
||||
$sentMails++;
|
||||
if (! $dryRun) {
|
||||
Mail::to($user->email)
|
||||
->queue(new AuthApiTokenExpirationReminderMail($token, $user));
|
||||
$token->reminder_sent_at = Carbon::now();
|
||||
$token->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
$this->comment('Finished sending '.$sentMails.' expiring API token emails...');
|
||||
|
||||
$this->comment('Sent emails about expired API tokens');
|
||||
$sentMails = 0;
|
||||
Token::query()
|
||||
->where('expires_at', '<=', Carbon::now())
|
||||
->whereNull('expired_info_sent_at')
|
||||
->with([
|
||||
'client',
|
||||
'user',
|
||||
])
|
||||
->whereHas('user', function (Builder $query): void {
|
||||
/** @var Builder<User> $query */
|
||||
$query->where('is_placeholder', '=', false);
|
||||
})
|
||||
->isApiToken(true)
|
||||
->orderBy('created_at', 'asc')
|
||||
->chunk(500, function (Collection $tokens) use ($dryRun, &$sentMails): void {
|
||||
/** @var Collection<int, Token> $tokens */
|
||||
foreach ($tokens as $token) {
|
||||
$user = $token->user;
|
||||
$this->info('Start sending email to user "'.$user->email.'" ('.$user->getKey().') about expired API token '.$token->getKey());
|
||||
$sentMails++;
|
||||
if (! $dryRun) {
|
||||
Mail::to($user->email)
|
||||
->queue(new AuthApiTokenExpiredMail($token, $user));
|
||||
$token->expired_info_sent_at = Carbon::now();
|
||||
$token->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
$this->comment('Finished sending '.$sentMails.' expired API token emails...');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ class Kernel extends ConsoleKernel
|
||||
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
|
||||
->everyTenMinutes();
|
||||
|
||||
$schedule->command('auth:send-mails-expiring-api-tokens')
|
||||
->when(fn (): bool => config('scheduling.tasks.auth_send_mails_expiring_api_tokens'))
|
||||
->everyTenMinutes();
|
||||
|
||||
$schedule->command('self-host:check-for-update')
|
||||
->when(fn (): bool => config('scheduling.tasks.self_hosting_check_for_update'))
|
||||
->twiceDaily();
|
||||
|
||||
@@ -41,9 +41,7 @@ class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! ($collectingType = $this->openApiTransformer->transform($collectingClassType))) {
|
||||
return null;
|
||||
}
|
||||
$collectingType = $this->openApiTransformer->transform($collectingClassType);
|
||||
|
||||
$newType = new OpenApiObjectType;
|
||||
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));
|
||||
|
||||
@@ -15,6 +15,7 @@ use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
@@ -75,7 +76,8 @@ class FailedJobResource extends Resource
|
||||
->filters([])
|
||||
->bulkActions([
|
||||
BulkAction::make('retry')
|
||||
->label('Retry')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->label('Retry selected')
|
||||
->requiresConfirmation()
|
||||
->action(function (Collection $records): void {
|
||||
/** @var FailedJob $record */
|
||||
@@ -87,11 +89,13 @@ class FailedJobResource extends Resource
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
DeleteBulkAction::make(),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make('Delete'),
|
||||
ViewAction::make('View'),
|
||||
DeleteAction::make(),
|
||||
ViewAction::make(),
|
||||
Action::make('retry')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->label('Retry')
|
||||
->requiresConfirmation()
|
||||
->action(function (FailedJob $record): void {
|
||||
@@ -109,7 +113,6 @@ class FailedJobResource extends Resource
|
||||
return [
|
||||
'index' => ListFailedJobs::route('/'),
|
||||
'view' => ViewFailedJobs::route('/{record}'),
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace App\Filament\Resources\FailedJobResource\Pages;
|
||||
|
||||
use App\Filament\Resources\FailedJobResource;
|
||||
use App\Models\FailedJob;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Actions\Action;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
@@ -19,7 +19,8 @@ class ListFailedJobs extends ListRecords
|
||||
{
|
||||
return [
|
||||
Action::make('retry_all')
|
||||
->label('Retry all failed Jobs')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->label('Retry all')
|
||||
->requiresConfirmation()
|
||||
->action(function (): void {
|
||||
Artisan::call('queue:retry all');
|
||||
@@ -30,7 +31,8 @@ class ListFailedJobs extends ListRecords
|
||||
}),
|
||||
|
||||
Action::make('delete_all')
|
||||
->label('Delete all failed Jobs')
|
||||
->icon('heroicon-o-trash')
|
||||
->label('Delete all')
|
||||
->requiresConfirmation()
|
||||
->color('danger')
|
||||
->action(function (): void {
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TokenResource\Pages;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
@@ -40,7 +39,7 @@ class TokenResource extends Resource
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('user_id')
|
||||
Forms\Components\Select::make('owner_id')
|
||||
->label('User')
|
||||
->relationship(name: 'user', titleAttribute: 'name')
|
||||
->searchable(['name'])
|
||||
@@ -79,10 +78,12 @@ class TokenResource extends Resource
|
||||
Tables\Columns\TextColumn::make('client.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('client.personal_access_client')
|
||||
Tables\Columns\IconColumn::make('personal_access_client')
|
||||
->state(function (Token $token): bool {
|
||||
return in_array('personal_access', $token->client->grant_types ?? [], true);
|
||||
})
|
||||
->boolean()
|
||||
->label('API token?')
|
||||
->sortable(),
|
||||
->label('API token?'),
|
||||
Tables\Columns\IconColumn::make('revoked')
|
||||
->boolean()
|
||||
->label('Revoked?')
|
||||
@@ -104,17 +105,11 @@ class TokenResource extends Resource
|
||||
->queries(
|
||||
true: function (Builder $query) {
|
||||
/** @var Builder<Token> $query */
|
||||
return $query->whereHas('client', function (Builder $query) {
|
||||
/** @var Builder<Client> $query */
|
||||
return $query->where('personal_access_client', true);
|
||||
});
|
||||
return $query->isApiToken();
|
||||
},
|
||||
false: function (Builder $query) {
|
||||
/** @var Builder<Token> $query */
|
||||
return $query->whereHas('client', function (Builder $query) {
|
||||
/** @var Builder<Client> $query */
|
||||
return $query->where('personal_access_client', false);
|
||||
});
|
||||
return $query->isApiToken(false);
|
||||
},
|
||||
blank: function (Builder $query) {
|
||||
/** @var Builder<Token> $query */
|
||||
|
||||
@@ -8,9 +8,12 @@ use App\Exceptions\Api\PersonalAccessClientIsNotConfiguredException;
|
||||
use App\Http\Requests\V1\ApiToken\ApiTokenStoreRequest;
|
||||
use App\Http\Resources\V1\ApiToken\ApiTokenCollection;
|
||||
use App\Http\Resources\V1\ApiToken\ApiTokenWithAccessTokenResource;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ApiTokenController extends Controller
|
||||
{
|
||||
@@ -28,7 +31,10 @@ class ApiTokenController extends Controller
|
||||
$user = $this->user();
|
||||
|
||||
$tokens = $user->tokens()
|
||||
->where('client_id', '=', config('passport.personal_access_client.id'))
|
||||
->whereHas('client', function (Builder $query): void {
|
||||
/** @var Builder<Client> $query */
|
||||
$query->whereJsonContains('grant_types', 'personal_access');
|
||||
})
|
||||
->get();
|
||||
|
||||
return new ApiTokenCollection($tokens);
|
||||
@@ -48,15 +54,21 @@ class ApiTokenController extends Controller
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
||||
throw new PersonalAccessClientIsNotConfiguredException;
|
||||
try {
|
||||
$token = $user->createToken($request->getName(), ['*']);
|
||||
|
||||
/** @var Token $tokenModel */
|
||||
$tokenModel = $token->getToken();
|
||||
|
||||
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
|
||||
} catch (\RuntimeException $exception) {
|
||||
report($exception);
|
||||
if (Str::contains($exception->getMessage(), ['Personal access client not found'])) {
|
||||
throw new PersonalAccessClientIsNotConfiguredException;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$token = $user->createToken($request->getName(), ['*']);
|
||||
/** @var Token $tokenModel */
|
||||
$tokenModel = $token->token;
|
||||
|
||||
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,13 +83,10 @@ class ApiTokenController extends Controller
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
||||
throw new PersonalAccessClientIsNotConfiguredException;
|
||||
}
|
||||
if ($apiToken->user_id !== $user->getKey()) {
|
||||
throw new AuthorizationException('API token does not belong to user');
|
||||
}
|
||||
if ($apiToken->client_id !== config('passport.personal_access_client.id')) {
|
||||
if (! ($apiToken->client?->hasGrantType('personal_access') ?? false)) {
|
||||
throw new AuthorizationException('API token is not a personal access token');
|
||||
}
|
||||
|
||||
@@ -97,13 +106,10 @@ class ApiTokenController extends Controller
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
||||
throw new PersonalAccessClientIsNotConfiguredException;
|
||||
}
|
||||
if ($apiToken->user_id !== $user->getKey()) {
|
||||
throw new AuthorizationException('API token does not belong to user');
|
||||
}
|
||||
if ($apiToken->client_id !== config('passport.personal_access_client.id')) {
|
||||
if (! ($apiToken->client?->hasGrantType('personal_access') ?? false)) {
|
||||
throw new AuthorizationException('API token is not a personal access token');
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
@@ -20,8 +19,7 @@ class EnsureEmailIsVerified
|
||||
{
|
||||
if (! app()->isLocal()) {
|
||||
if ($request->user() === null ||
|
||||
($request->user() instanceof MustVerifyEmail &&
|
||||
! $request->user()->hasVerifiedEmail())) {
|
||||
(! $request->user()->hasVerifiedEmail())) {
|
||||
return $request->expectsJson()
|
||||
? abort(403, 'Your email address is not verified.')
|
||||
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
|
||||
|
||||
@@ -50,7 +50,7 @@ class HandleInertiaRequests extends Middleware
|
||||
return array_merge(parent::share($request), [
|
||||
'has_billing_extension' => $hasBilling,
|
||||
'has_invoicing_extension' => $hasInvoicing,
|
||||
'billing' => $billing !== null && $currentOrganization !== null ? [
|
||||
'billing' => $currentOrganization !== null ? [
|
||||
'has_subscription' => $billing->hasSubscription($currentOrganization),
|
||||
'has_trial' => $billing->hasTrial($currentOrganization),
|
||||
'trial_until' => $billing->getTrialUntil($currentOrganization)?->toIso8601ZuluString(),
|
||||
|
||||
@@ -26,7 +26,7 @@ class ShareInertiaData
|
||||
{
|
||||
/** @var PermissionStore $permissions */
|
||||
$permissions = app(PermissionStore::class);
|
||||
Inertia::share(array_filter([
|
||||
Inertia::share([
|
||||
'jetstream' => function () use ($request) {
|
||||
/** @var User|null $user */
|
||||
$user = $request->user();
|
||||
@@ -101,7 +101,7 @@ class ShareInertiaData
|
||||
return [$key => $bag->messages()];
|
||||
})->all();
|
||||
},
|
||||
]));
|
||||
]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
@@ -8,15 +8,11 @@ use App\Http\Resources\PaginatedResourceCollection;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class ProjectCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
private bool $showBillableRates;
|
||||
|
||||
/**
|
||||
* @param LengthAwarePaginator<Project> $resource
|
||||
*/
|
||||
public function __construct($resource, bool $showBillableRates)
|
||||
{
|
||||
parent::__construct($resource);
|
||||
|
||||
44
app/Mail/AuthApiTokenExpirationReminderMail.php
Normal file
44
app/Mail/AuthApiTokenExpirationReminderMail.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Passport\Token;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class AuthApiTokenExpirationReminderMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public Token $token;
|
||||
|
||||
public User $user;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Token $token, User $user)
|
||||
{
|
||||
$this->token = $token;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*/
|
||||
public function build(): self
|
||||
{
|
||||
return $this->markdown('emails.auth-api-expiration-reminder', [
|
||||
'profileUrl' => URL::to('user/profile'),
|
||||
'tokenName' => $this->token->name,
|
||||
])
|
||||
->subject(__('Your API token will expire in 7 days!'));
|
||||
}
|
||||
}
|
||||
44
app/Mail/AuthApiTokenExpiredMail.php
Normal file
44
app/Mail/AuthApiTokenExpiredMail.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Passport\Token;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class AuthApiTokenExpiredMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public Token $token;
|
||||
|
||||
public User $user;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Token $token, User $user)
|
||||
{
|
||||
$this->token = $token;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*/
|
||||
public function build(): self
|
||||
{
|
||||
return $this->markdown('emails.auth-api-token-expired', [
|
||||
'profileUrl' => URL::to('user/profile'),
|
||||
'tokenName' => $this->token->name,
|
||||
])
|
||||
->subject(__('Your API token has expired!'));
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ use OwenIt\Auditing\Models\Audit as PackageAuditModel;
|
||||
* @property string $event
|
||||
* @property string $auditable_type
|
||||
* @property string $auditable_id
|
||||
* @property array|null $old_values
|
||||
* @property array|null $new_values
|
||||
* @property array<string, mixed>|null $old_values
|
||||
* @property array<string, mixed>|null $new_values
|
||||
* @property string|null $url
|
||||
* @property string|null $ip_address
|
||||
* @property string|null $user_agent
|
||||
|
||||
@@ -47,7 +47,7 @@ class Client extends Model implements AuditableContract
|
||||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, Client>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
@@ -55,7 +55,7 @@ class Client extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Project>
|
||||
* @return HasMany<Project, $this>
|
||||
*/
|
||||
public function projects(): HasMany
|
||||
{
|
||||
|
||||
@@ -25,8 +25,8 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read Organization $organization
|
||||
* @property-read User $user
|
||||
* @property-read Collection<ProjectMember> $projectMembers
|
||||
* @property-read Collection<TimeEntry> $timeEntries
|
||||
* @property-read Collection<int, ProjectMember> $projectMembers
|
||||
* @property-read Collection<int, TimeEntry> $timeEntries
|
||||
*
|
||||
* @method static MemberFactory factory()
|
||||
*/
|
||||
@@ -47,7 +47,7 @@ class Member extends JetstreamMembership implements AuditableContract
|
||||
protected $table = 'members';
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, Member>
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
@@ -55,7 +55,7 @@ class Member extends JetstreamMembership implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, Member>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
@@ -63,7 +63,7 @@ class Member extends JetstreamMembership implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<TimeEntry>
|
||||
* @return HasMany<TimeEntry, $this>
|
||||
*/
|
||||
public function timeEntries(): HasMany
|
||||
{
|
||||
@@ -71,7 +71,7 @@ class Member extends JetstreamMembership implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<ProjectMember>
|
||||
* @return HasMany<ProjectMember, $this>
|
||||
*/
|
||||
public function projectMembers(): HasMany
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Events\TeamCreated;
|
||||
@@ -47,7 +48,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @property IntervalFormat $interval_format
|
||||
* @property TimeFormat $time_format
|
||||
*
|
||||
* @method HasMany<OrganizationInvitation> teamInvitations()
|
||||
* @method HasMany<OrganizationInvitation, $this> teamInvitations()
|
||||
* @method static OrganizationFactory factory()
|
||||
*/
|
||||
class Organization extends JetstreamTeam implements AuditableContract
|
||||
@@ -79,7 +80,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
@@ -125,7 +126,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
||||
/**
|
||||
* Get all the users that belong to the team.
|
||||
*
|
||||
* @return BelongsToMany<User>
|
||||
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
@@ -142,7 +143,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
||||
/**
|
||||
* Get the owner of the team.
|
||||
*
|
||||
* @return BelongsTo<User, Organization>
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
@@ -150,7 +151,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Member>
|
||||
* @return HasMany<Member, $this>
|
||||
*/
|
||||
public function members(): HasMany
|
||||
{
|
||||
@@ -158,7 +159,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<User>
|
||||
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||
*/
|
||||
public function realUsers(): BelongsToMany
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ class OrganizationInvitation extends JetstreamTeamInvitation implements Auditabl
|
||||
/**
|
||||
* Get the organization that the invitation belongs to.
|
||||
*
|
||||
* @return BelongsTo<Organization, OrganizationInvitation>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
@@ -63,7 +63,7 @@ class OrganizationInvitation extends JetstreamTeamInvitation implements Auditabl
|
||||
/**
|
||||
* Get the organization that the invitation belongs to.
|
||||
*
|
||||
* @return BelongsTo<Organization, OrganizationInvitation>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function team(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -4,6 +4,26 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Laravel\Passport\AuthCode as PassportAuthCode;
|
||||
|
||||
class AuthCode extends PassportAuthCode {}
|
||||
/**
|
||||
* @property string $id
|
||||
* @property string $user_id
|
||||
* @property string $client_id
|
||||
* @property string|null $scopes
|
||||
* @property bool $revoked
|
||||
* @property Carbon $expires_at
|
||||
*/
|
||||
class AuthCode extends PassportAuthCode
|
||||
{
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,36 @@ declare(strict_types=1);
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use Database\Factories\Passport\ClientFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Laravel\Passport\Client as PassportClient;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
* @property string|null $user_id
|
||||
* @property string|null $owner_id
|
||||
* @property string|null $owner_type
|
||||
* @property string $name
|
||||
* @property string|null $secret
|
||||
* @property string|null $provider
|
||||
* @property string $redirect
|
||||
* @property bool $personal_access_client
|
||||
* @property bool $password_client
|
||||
* @property array<string> $grant_types
|
||||
* @property array<string> $redirect_uris
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property bool $revoked
|
||||
*/
|
||||
class Client extends PassportClient
|
||||
{
|
||||
/** @use HasFactory<ClientFactory> */
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Create a new factory instance for the model.
|
||||
*
|
||||
* @return ClientFactory
|
||||
*/
|
||||
protected static function newFactory(): Factory
|
||||
{
|
||||
return ClientFactory::new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use Laravel\Passport\PersonalAccessClient as PassportPersonalAccessClient;
|
||||
|
||||
class PersonalAccessClient extends PassportPersonalAccessClient {}
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Factories\Passport\TokenFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -17,9 +19,15 @@ use Laravel\Passport\Token as PassportToken;
|
||||
* @property null|string $name
|
||||
* @property array<string> $scopes
|
||||
* @property bool $revoked
|
||||
* @property Carbon|null $reminder_sent_at
|
||||
* @property Carbon|null $expired_info_sent_at
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Carbon|null $expires_at
|
||||
* @property-read Client|null $client
|
||||
* @property-read User|null $user
|
||||
*
|
||||
* @method Builder<Token> isApiToken(bool $isApiToken = true)
|
||||
*/
|
||||
class Token extends PassportToken
|
||||
{
|
||||
@@ -29,10 +37,60 @@ class Token extends PassportToken
|
||||
/**
|
||||
* Get the client that the token belongs to.
|
||||
*
|
||||
* @return BelongsTo<Client, Token>
|
||||
* @return BelongsTo<Client, $this>
|
||||
*/
|
||||
// @phpstan-ignore method.childReturnType
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class, 'client_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user that the token belongs to.
|
||||
*
|
||||
* @deprecated Will be removed in a future Laravel version.
|
||||
*
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
// @phpstan-ignore method.childReturnType
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'scopes' => 'array',
|
||||
'revoked' => 'bool',
|
||||
'expires_at' => 'datetime',
|
||||
'reminder_sent_at' => 'datetime',
|
||||
'expired_info_sent_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<static> $query
|
||||
* @return Builder<static>
|
||||
*/
|
||||
public function scopeIsApiToken(Builder $query, bool $isApiToken = true): Builder
|
||||
{
|
||||
if ($isApiToken) {
|
||||
return $query->whereHas('client', function (Builder $query): void {
|
||||
/** @var Builder<Client> $query */
|
||||
$query->whereJsonContains('grant_types', 'personal_access');
|
||||
});
|
||||
} else {
|
||||
return $query->whereHas('client', function (Builder $query): void {
|
||||
/** @var Builder<Client> $query */
|
||||
$query->whereJsonDoesntContain('grant_types', 'personal_access');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ class Project extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, Project>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
@@ -145,7 +145,7 @@ class Project extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Client, Project>
|
||||
* @return BelongsTo<Client, $this>
|
||||
*/
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
@@ -153,7 +153,7 @@ class Project extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<ProjectMember>
|
||||
* @return HasMany<ProjectMember, $this>
|
||||
*/
|
||||
public function members(): HasMany
|
||||
{
|
||||
@@ -161,7 +161,7 @@ class Project extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Task>
|
||||
* @return HasMany<Task, $this>
|
||||
*/
|
||||
public function tasks(): HasMany
|
||||
{
|
||||
@@ -169,7 +169,7 @@ class Project extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<TimeEntry>
|
||||
* @return HasMany<TimeEntry, $this>
|
||||
*/
|
||||
public function timeEntries(): HasMany
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ class ProjectMember extends Model implements AuditableContract
|
||||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Project, ProjectMember>
|
||||
* @return BelongsTo<Project, $this>
|
||||
*/
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
@@ -58,7 +58,7 @@ class ProjectMember extends Model implements AuditableContract
|
||||
/**
|
||||
* @deprecated Use member relationship instead
|
||||
*
|
||||
* @return BelongsTo<User, ProjectMember>
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
@@ -66,7 +66,7 @@ class ProjectMember extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, ProjectMember>
|
||||
* @return BelongsTo<Member, $this>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -55,7 +55,7 @@ class Report extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, Report>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ use Staudenmeir\EloquentJsonRelations\Relations\HasManyJson;
|
||||
* @property string $organization_id
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read Collection<TimeEntry> $timeEntries
|
||||
* @property-read Collection<int, TimeEntry> $timeEntries
|
||||
* @property-read Organization $organization
|
||||
*
|
||||
* @method static TagFactory factory()
|
||||
@@ -47,7 +47,7 @@ class Tag extends Model implements AuditableContract
|
||||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, Tag>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -120,7 +120,7 @@ class Task extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Project, Task>
|
||||
* @return BelongsTo<Project, $this>
|
||||
*/
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
@@ -128,7 +128,7 @@ class Task extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, Task>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
@@ -136,7 +136,7 @@ class Task extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<TimeEntry>
|
||||
* @return HasMany<TimeEntry, $this>
|
||||
*/
|
||||
public function timeEntries(): HasMany
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ use Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson;
|
||||
* @property Carbon|null $end
|
||||
* @property int|null $billable_rate Billable rate per hour in cents
|
||||
* @property bool $billable
|
||||
* @property array $tags
|
||||
* @property array<string> $tags
|
||||
* @property string $user_id
|
||||
* @property string $member_id
|
||||
* @property bool $is_imported
|
||||
@@ -45,7 +45,7 @@ use Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson;
|
||||
* @property-read Client|null $client
|
||||
* @property string|null $task_id
|
||||
* @property-read Task|null $task
|
||||
* @property-read Collection<Tag> $tagsRelation
|
||||
* @property-read Collection<int, Tag> $tagsRelation
|
||||
*
|
||||
* @method Builder<TimeEntry> hasTag(Tag $tag)
|
||||
* @method static TimeEntryFactory factory()
|
||||
@@ -154,7 +154,7 @@ class TimeEntry extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, TimeEntry>
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
@@ -162,7 +162,7 @@ class TimeEntry extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, TimeEntry>
|
||||
* @return BelongsTo<Member, $this>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
@@ -170,7 +170,7 @@ class TimeEntry extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, TimeEntry>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
@@ -178,7 +178,7 @@ class TimeEntry extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Project, TimeEntry>
|
||||
* @return BelongsTo<Project, $this>
|
||||
*/
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
@@ -186,7 +186,7 @@ class TimeEntry extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Task, TimeEntry>
|
||||
* @return BelongsTo<Task, $this>
|
||||
*/
|
||||
public function task(): BelongsTo
|
||||
{
|
||||
@@ -196,7 +196,7 @@ class TimeEntry extends Model implements AuditableContract
|
||||
/**
|
||||
* This relation can be reconstructed via the task relation. It is only here for performance reasons.
|
||||
*
|
||||
* @return BelongsTo<Client, TimeEntry>
|
||||
* @return BelongsTo<Client, $this>
|
||||
*/
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -27,6 +28,7 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Jetstream\HasTeams;
|
||||
use Laravel\Passport\AuthCode;
|
||||
use Laravel\Passport\Contracts\OAuthenticatable;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
|
||||
@@ -52,13 +54,13 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @property Collection<int, TimeEntry> $timeEntries
|
||||
* @property Member $membership
|
||||
*
|
||||
* @method HasMany<Organization> ownedTeams()
|
||||
* @method HasMany<Organization, $this> ownedTeams()
|
||||
* @method static UserFactory factory()
|
||||
* @method static Builder<User> query()
|
||||
* @method Builder<User> belongsToOrganization(Organization $organization)
|
||||
* @method Builder<User> active()
|
||||
*/
|
||||
class User extends Authenticatable implements AuditableContract, FilamentUser, MustVerifyEmail
|
||||
class User extends Authenticatable implements AuditableContract, FilamentUser, MustVerifyEmail, OAuthenticatable
|
||||
{
|
||||
use CustomAuditable;
|
||||
use HasApiTokens;
|
||||
@@ -75,7 +77,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
@@ -86,7 +88,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
@@ -143,7 +145,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Organization>
|
||||
* @return BelongsToMany<Organization, $this, Pivot, 'membership'>
|
||||
*/
|
||||
public function organizations(): BelongsToMany
|
||||
{
|
||||
@@ -158,7 +160,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<TimeEntry>
|
||||
* @return HasMany<TimeEntry, $this>
|
||||
*/
|
||||
public function timeEntries(): HasMany
|
||||
{
|
||||
@@ -166,7 +168,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, User>
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function currentOrganization(): BelongsTo
|
||||
{
|
||||
@@ -174,7 +176,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<ProjectMember>
|
||||
* @return HasMany<ProjectMember, $this>
|
||||
*/
|
||||
public function projectMembers(): HasMany
|
||||
{
|
||||
@@ -182,7 +184,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Token>
|
||||
* @return HasMany<Token, $this>
|
||||
*/
|
||||
public function accessTokens(): HasMany
|
||||
{
|
||||
@@ -190,24 +192,13 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<AuthCode>
|
||||
* @return HasMany<AuthCode, $this>
|
||||
*/
|
||||
public function authCodes(): HasMany
|
||||
{
|
||||
return $this->hasMany(AuthCode::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the access tokens for the user.
|
||||
*
|
||||
* @return HasMany<Token>
|
||||
*/
|
||||
public function tokens(): HasMany
|
||||
{
|
||||
return $this->hasMany(Token::class, 'user_id')
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<User> $builder
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace App\Providers;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Passport\AuthCode;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\PersonalAccessClient;
|
||||
use App\Models\Passport\RefreshToken;
|
||||
use App\Models\Passport\Token;
|
||||
use App\Policies\OrganizationPolicy;
|
||||
@@ -51,7 +50,8 @@ class AuthServiceProvider extends ServiceProvider
|
||||
Passport::useRefreshTokenModel(RefreshToken::class);
|
||||
Passport::useAuthCodeModel(AuthCode::class);
|
||||
Passport::useClientModel(Client::class);
|
||||
Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
|
||||
|
||||
Passport::authorizationView('auth.oauth.authorize');
|
||||
|
||||
// Passport::tokensExpireIn(now()->addDays(15));
|
||||
// Passport::refreshTokensExpireIn(now()->addDays(30));
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Brick\Money\ISOCurrencyProvider;
|
||||
use Brick\Money\Money;
|
||||
|
||||
class CurrencyService
|
||||
@@ -374,4 +375,12 @@ class CurrencyService
|
||||
|
||||
return $currencyCode;
|
||||
}
|
||||
|
||||
public function getRandomCurrencyCode(): string
|
||||
{
|
||||
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$currencyCodes = array_keys($currencies);
|
||||
|
||||
return $currencyCodes[array_rand($currencyCodes)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +119,6 @@ class ReportPropertiesDto implements Castable
|
||||
return $dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ReportPropertiesDto $value
|
||||
*/
|
||||
public function set(Model $model, string $key, mixed $value, array $attributes): string
|
||||
{
|
||||
if (! ($value instanceof ReportPropertiesDto)) {
|
||||
|
||||
@@ -71,7 +71,7 @@ class PermissionStore
|
||||
/** @var Role|null $roleObj */
|
||||
$roleObj = Jetstream::findRole($role);
|
||||
|
||||
return $roleObj?->permissions ?? [];
|
||||
return $roleObj->permissions ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,31 +11,31 @@
|
||||
"datomatic/laravel-enum-helper": "^2.0.0",
|
||||
"dedoc/scramble": "^0.12.2",
|
||||
"filament/filament": "^3.2",
|
||||
"flowframe/laravel-trend": "^0.3.0",
|
||||
"flowframe/laravel-trend": "^0.4.0",
|
||||
"gotenberg/gotenberg-php": "^2.8",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"inertiajs/inertia-laravel": "^1.0",
|
||||
"inertiajs/inertia-laravel": "^2.0.3",
|
||||
"korridor/laravel-computed-attributes": "^3.1",
|
||||
"korridor/laravel-has-many-sync": "^3.1",
|
||||
"korridor/laravel-model-validation-rules": "^3.0",
|
||||
"laravel/framework": "^11.16.0",
|
||||
"laravel/framework": "^12.19.3",
|
||||
"laravel/jetstream": "^5.0",
|
||||
"laravel/octane": "^2.3",
|
||||
"laravel/passport": "^12.0",
|
||||
"laravel/passport": "^13.0.5",
|
||||
"laravel/tinker": "^2.8",
|
||||
"league/csv": "^9.16.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"league/iso3166": "^4.3",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"novadaemon/filament-pretty-json": "^2.2",
|
||||
"nwidart/laravel-modules": "^11.0.11",
|
||||
"owen-it/laravel-auditing": "^13.6",
|
||||
"pxlrbt/filament-environment-indicator": "^2.0",
|
||||
"nwidart/laravel-modules": "^12.0.4",
|
||||
"owen-it/laravel-auditing": "^14.0.0",
|
||||
"pxlrbt/filament-environment-indicator": "^2.1.0",
|
||||
"spatie/temporary-directory": "^2.2",
|
||||
"staudenmeir/eloquent-json-relations": "^1.1",
|
||||
"stechstudio/filament-impersonate": "^3.8",
|
||||
"tightenco/ziggy": "^2.1.0",
|
||||
"tpetry/laravel-postgresql-enhanced": "^2.0.0",
|
||||
"tpetry/laravel-postgresql-enhanced": "^3.0.0",
|
||||
"wikimedia/composer-merge-plugin": "^2.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -43,14 +43,13 @@
|
||||
"brianium/paratest": "^7.3",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"fumeapp/modeltyper": "^3.0",
|
||||
"phpstan/phpstan": "1.12.0",
|
||||
"larastan/larastan": "^2.0",
|
||||
"larastan/larastan": "^3.5.0",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/sail": "^1.18",
|
||||
"laravel/telescope": "^5.0",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"phpunit/phpunit": "^11",
|
||||
"phpunit/phpunit": "^12",
|
||||
"spatie/laravel-ignition": "^2.0",
|
||||
"timacdonald/log-fake": "^2.1"
|
||||
},
|
||||
|
||||
2458
composer.lock
generated
2458
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -34,31 +34,15 @@ return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Client UUIDs
|
||||
| Passport Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default, Passport uses auto-incrementing primary keys when assigning
|
||||
| IDs to clients. However, if Passport is installed using the provided
|
||||
| --uuids switch, this will be set to "true" and UUIDs will be used.
|
||||
| By default, Passport's models will utilize your application's default
|
||||
| database connection. If you wish to use a different connection you
|
||||
| may specify the configured name of the database connection here.
|
||||
|
|
||||
*/
|
||||
|
||||
'client_uuids' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Personal Access Client
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you enable client hashing, you should set the personal access client
|
||||
| ID and unhashed secret within your environment file. The values will
|
||||
| get used while issuing fresh personal access tokens to your users.
|
||||
|
|
||||
*/
|
||||
|
||||
'personal_access_client' => [
|
||||
'id' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_ID'),
|
||||
'secret' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET'),
|
||||
],
|
||||
'connection' => env('PASSPORT_CONNECTION'),
|
||||
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ return [
|
||||
|
||||
'tasks' => [
|
||||
'time_entry_send_still_running_mails' => (bool) env('SCHEDULING_TASK_TIME_ENTRY_SEND_STILL_RUNNING_MAILS', true),
|
||||
'auth_send_mails_expiring_api_tokens' => (bool) env('SCHEDULING_TASK_AUTH_SEND_MAILS_EXPIRING_API_TOKENS', true),
|
||||
'self_hosting_check_for_update' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_CHECK_FOR_UPDATE', true),
|
||||
'self_hosting_telemetry' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_TELEMETRY', true),
|
||||
'self_hosting_database_consistency' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_DATABASE_CONSISTENCY', false),
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Service\CurrencyService;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,7 @@ class OrganizationFactory extends Factory
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->unique()->company(),
|
||||
'currency' => $this->faker->currencyCode(),
|
||||
'currency' => app(CurrencyService::class)->getRandomCurrencyCode(),
|
||||
'billable_rate' => null,
|
||||
'user_id' => User::factory(),
|
||||
'personal_team' => true,
|
||||
|
||||
@@ -7,11 +7,12 @@ namespace Database\Factories\Passport;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Laravel\Passport\Database\Factories\ClientFactory as BaseClientFactory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Client>
|
||||
*/
|
||||
class ClientFactory extends Factory
|
||||
class ClientFactory extends BaseClientFactory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
@@ -22,24 +23,40 @@ class ClientFactory extends Factory
|
||||
{
|
||||
return [
|
||||
'id' => $this->faker->uuid,
|
||||
'user_id' => null,
|
||||
'owner_id' => null,
|
||||
'owner_type' => null,
|
||||
'name' => $this->faker->company(),
|
||||
'secret' => $this->faker->regexify('[A-Za-z]{40}'),
|
||||
'provider' => 'users',
|
||||
'redirect' => $this->faker->url(),
|
||||
'personal_access_client' => false,
|
||||
'password_client' => false,
|
||||
'redirect_uris' => [$this->faker->url()],
|
||||
'grant_types' => [],
|
||||
'revoked' => false,
|
||||
'created_at' => $this->faker->dateTime(),
|
||||
'updated_at' => $this->faker->dateTime(),
|
||||
];
|
||||
}
|
||||
|
||||
public function desktopClient(): self
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'name' => 'Desktop',
|
||||
'grant_types' => ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token', 'authorization_code', 'implicit'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function apiClient(): self
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'name' => 'API',
|
||||
'grant_types' => ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token', 'client_credentials', 'personal_access'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function personalAccessClient(): self
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
'personal_access_client' => true,
|
||||
'grant_types' => ['personal_access'],
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -48,7 +65,8 @@ class ClientFactory extends Factory
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($user): array {
|
||||
return [
|
||||
'user_id' => $user->getKey(),
|
||||
'owner_id' => $user->getKey(),
|
||||
'owner_type' => (new User)->getMorphClass(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ class TokenFactory extends Factory
|
||||
'created_at' => $this->faker->dateTime,
|
||||
'updated_at' => $this->faker->dateTime,
|
||||
'expires_at' => $this->faker->dateTime,
|
||||
'reminder_sent_at' => null,
|
||||
'expired_info_sent_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('oauth_device_codes', function (Blueprint $table): void {
|
||||
$table->char('id', 80)->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->foreignUuid('client_id')->index();
|
||||
$table->char('user_code', 8)->unique();
|
||||
$table->text('scopes');
|
||||
$table->boolean('revoked');
|
||||
$table->dateTime('user_approved_at')->nullable();
|
||||
$table->dateTime('last_polled_at')->nullable();
|
||||
$table->dateTime('expires_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('oauth_device_codes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the migration connection name.
|
||||
*/
|
||||
public function getConnection(): ?string
|
||||
{
|
||||
return $this->connection ?? config('passport.connection');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::drop('oauth_personal_access_clients');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::create('oauth_personal_access_clients', function (Blueprint $table): void {
|
||||
$table->bigIncrements('id');
|
||||
$table->uuid('client_id');
|
||||
$table->foreign('client_id')
|
||||
->references('id')
|
||||
->on('oauth_clients')
|
||||
->onDelete('restrict')
|
||||
->onUpdate('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('oauth_clients')->update(['provider' => 'users']); // Change default provider if necessary
|
||||
|
||||
Schema::table('oauth_clients', function (Blueprint $table): void {
|
||||
$table->text('grant_types')->default('[]')->after('provider');
|
||||
$table->text('redirect_uris')->default('[]');
|
||||
$table->renameColumn('user_id', 'owner_id');
|
||||
$table->string('owner_type')->after('owner_id')->nullable();
|
||||
});
|
||||
|
||||
DB::table('oauth_clients')
|
||||
->where('redirect', '=', 'http://localhost')
|
||||
->where('personal_access_client', '=', true)
|
||||
->update(['redirect' => '']);
|
||||
|
||||
DB::table('oauth_clients')
|
||||
->whereNotNull('owner_id')
|
||||
->update(['owner_type' => 'user']); // Value might be class name of the owner model, depends on if you use "enforceMorphMap"
|
||||
|
||||
DB::table('oauth_clients')->eachById(function ($client): void {
|
||||
$grantTypes = ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'];
|
||||
$confidential = ! empty($client->secret);
|
||||
$noRedirect = empty($client->redirect);
|
||||
$redirectUris = $noRedirect ? [] : [$client->redirect];
|
||||
$firstParty = empty($client->owner_id);
|
||||
|
||||
if (! $noRedirect) {
|
||||
$grantTypes[] = 'authorization_code';
|
||||
$grantTypes[] = 'implicit';
|
||||
}
|
||||
|
||||
if ($confidential && $firstParty) {
|
||||
$grantTypes[] = 'client_credentials';
|
||||
}
|
||||
|
||||
if ($client->personal_access_client && $confidential) {
|
||||
$grantTypes[] = 'personal_access';
|
||||
}
|
||||
|
||||
if ($client->password_client) {
|
||||
$grantTypes[] = 'password';
|
||||
}
|
||||
|
||||
DB::table('oauth_clients')
|
||||
->where('id', $client->id)
|
||||
->update([
|
||||
'redirect_uris' => $redirectUris,
|
||||
'grant_types' => $grantTypes,
|
||||
]);
|
||||
});
|
||||
|
||||
Schema::table('oauth_clients', function (Blueprint $table): void {
|
||||
$table->dropForeign(['user_id']);
|
||||
$table->index(['owner_id', 'owner_type']);
|
||||
$table->dropColumn('redirect');
|
||||
$table->dropColumn('personal_access_client');
|
||||
$table->dropColumn('password_client');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('oauth_clients', function (Blueprint $table): void {
|
||||
$table->dropIndex(['owner_id', 'owner_type']);
|
||||
$table->renameColumn('owner_id', 'user_id');
|
||||
$table->foreign('user_id')
|
||||
->on('users')
|
||||
->references('id')
|
||||
->onDelete('cascade')
|
||||
->onUpdate('cascade');
|
||||
$table->string('redirect')->nullable();
|
||||
$table->boolean('personal_access_client')->default(false);
|
||||
$table->boolean('password_client')->default(false);
|
||||
});
|
||||
|
||||
DB::table('oauth_clients')->eachById(function ($client): void {
|
||||
$redirectUris = json_decode($client->redirect_uris);
|
||||
$grantTypes = json_decode($client->grant_types);
|
||||
|
||||
DB::table('oauth_clients')
|
||||
->where('id', $client->id)
|
||||
->update([
|
||||
'redirect' => $redirectUris[0] ?? '', // redirect not nullable
|
||||
'password_client' => in_array('password', $grantTypes, true)
|
||||
&& in_array('refresh_token', $grantTypes, true),
|
||||
'personal_access_client' => in_array('personal_access', $grantTypes, true),
|
||||
]);
|
||||
});
|
||||
|
||||
Schema::table('oauth_clients', function (Blueprint $table): void {
|
||||
$table->dropColumn(['grant_types', 'redirect_uris', 'owner_type']);
|
||||
$table->string('redirect')->nullable(false)->change();
|
||||
$table->boolean('personal_access_client')->default(null)->change();
|
||||
$table->boolean('password_client')->default(null)->change();
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
35
database/migrations/2025_07_15_105949_hash_oauth_clients.php
Normal file
35
database/migrations/2025_07_15_105949_hash_oauth_clients.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// This could be optimized to run all the updates in the eachById
|
||||
DB::table('oauth_clients')->whereNotNull('secret')->eachById(function ($client): void {
|
||||
$secret = $client->secret;
|
||||
if (Hash::isHashed($secret) && ! Hash::needsRehash($secret)) {
|
||||
return; // Already hashed and not needing rehash
|
||||
}
|
||||
DB::table('oauth_clients')
|
||||
->where('id', $client->id)
|
||||
->update([
|
||||
'secret' => Hash::make($secret),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// This can not be reversed without a backup of the original secrets, for security reasons.
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('oauth_access_tokens', function (Blueprint $table): void {
|
||||
$table->dateTime('reminder_sent_at')->nullable();
|
||||
$table->dateTime('expired_info_sent_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('oauth_access_tokens', function (Blueprint $table): void {
|
||||
$table->dropColumn('reminder_sent_at');
|
||||
$table->dropColumn('expired_info_sent_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -24,7 +24,6 @@ use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Passport\AuthCode;
|
||||
use Laravel\Passport\Client as PassportClient;
|
||||
use Laravel\Passport\ClientRepository;
|
||||
use Laravel\Passport\PersonalAccessClient;
|
||||
use Laravel\Passport\RefreshToken;
|
||||
use Laravel\Passport\Token;
|
||||
|
||||
@@ -37,6 +36,18 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$this->deleteAll();
|
||||
|
||||
app(ClientRepository::class)->createAuthorizationCodeGrantClient(
|
||||
name: 'Desktop App',
|
||||
redirectUris: ['solidtime://oauth/callback'],
|
||||
confidential: false, // TODO: ?
|
||||
enableDeviceFlow: false, // TODO: ?
|
||||
);
|
||||
|
||||
// TODO: grant_types ? migration?
|
||||
|
||||
// app(ClientRepository::class)->createPersonalAccessGrantClient('API');
|
||||
|
||||
/*
|
||||
app(ClientRepository::class)->create(
|
||||
null,
|
||||
'desktop',
|
||||
@@ -46,17 +57,16 @@ class DatabaseSeeder extends Seeder
|
||||
false,
|
||||
false
|
||||
);
|
||||
*/
|
||||
|
||||
$personalAccessClient = new PassportClient;
|
||||
$personalAccessClient->id = config('passport.personal_access_client.id');
|
||||
$personalAccessClient->secret = config('passport.personal_access_client.secret');
|
||||
$personalAccessClient->name = 'API';
|
||||
$personalAccessClient->redirect = 'http://localhost';
|
||||
$personalAccessClient->user_id = null;
|
||||
$personalAccessClient->redirect_uris = ['http://localhost'];
|
||||
$personalAccessClient->revoked = false;
|
||||
$personalAccessClient->provider = null;
|
||||
$personalAccessClient->personal_access_client = true;
|
||||
$personalAccessClient->password_client = false;
|
||||
$personalAccessClient->provider = 'users';
|
||||
$personalAccessClient->grant_types = ['personal_access'];
|
||||
$personalAccessClient->save();
|
||||
|
||||
$userWithMultipleOrganizations = User::factory()->withPersonalOrganization()->create([
|
||||
@@ -197,7 +207,6 @@ class DatabaseSeeder extends Seeder
|
||||
DB::table((new RefreshToken)->getTable())->delete();
|
||||
DB::table((new Token)->getTable())->delete();
|
||||
DB::table((new AuthCode)->getTable())->delete();
|
||||
DB::table((new PersonalAccessClient)->getTable())->delete();
|
||||
DB::table((new PassportClient)->getTable())->delete();
|
||||
|
||||
// Internal tables
|
||||
|
||||
@@ -1,47 +1,30 @@
|
||||
# Accepted values: 8.3 - 8.2
|
||||
ARG PHP_VERSION=8.3
|
||||
|
||||
ARG FRANKENPHP_VERSION=latest
|
||||
|
||||
ARG COMPOSER_VERSION=latest
|
||||
|
||||
ARG FRANKENPHP_VERSION=1.8
|
||||
ARG COMPOSER_VERSION=2.8
|
||||
ARG BUN_VERSION="latest"
|
||||
ARG APP_ENV
|
||||
ARG DOCKER_FILES_BASE_PATH="docker/prod/"
|
||||
|
||||
###########################################
|
||||
# Build frontend assets with NPM
|
||||
###########################################
|
||||
|
||||
#ARG NODE_VERSION=20-alpine
|
||||
#
|
||||
#FROM node:${NODE_VERSION} AS build
|
||||
#
|
||||
#ENV ROOT=/var/www/html
|
||||
#
|
||||
#WORKDIR ${ROOT}
|
||||
#
|
||||
#RUN npm config set update-notifier false && npm set progress=false
|
||||
#
|
||||
#COPY package*.json ./
|
||||
#
|
||||
#RUN if [ -f $ROOT/package-lock.json ]; \
|
||||
# then \
|
||||
# npm ci --loglevel=error --no-audit; \
|
||||
# else \
|
||||
# npm install --loglevel=error --no-audit; \
|
||||
# fi
|
||||
#
|
||||
#COPY . .
|
||||
#
|
||||
#RUN npm run build
|
||||
|
||||
###########################################
|
||||
|
||||
FROM composer:${COMPOSER_VERSION} AS vendor
|
||||
|
||||
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION}
|
||||
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-builder-php${PHP_VERSION} AS upstream
|
||||
|
||||
ARG DOCKER_FILES_BASE_PATH
|
||||
ARG TARGETPLATFORM
|
||||
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
|
||||
|
||||
RUN CGO_ENABLED=1 \
|
||||
XCADDY_SETCAP=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output /usr/local/bin/frankenphp \
|
||||
--with github.com/dunglas/frankenphp=./ \
|
||||
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
|
||||
--with github.com/dunglas/caddy-cbrotli
|
||||
|
||||
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} AS base
|
||||
|
||||
COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||
|
||||
LABEL maintainer="solidtime <hello@solidtime.io>"
|
||||
LABEL org.opencontainers.image.title="solidtime"
|
||||
@@ -53,18 +36,22 @@ ARG WWWUSER=1000
|
||||
ARG WWWGROUP=1000
|
||||
ARG TZ=UTC
|
||||
ARG APP_DIR=/var/www/html
|
||||
ARG APP_ENV
|
||||
ARG APP_HOST
|
||||
ARG DOCKER_FILES_BASE_PATH
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
TERM=xterm-color \
|
||||
WITH_HORIZON=false \
|
||||
WITH_SCHEDULER=false \
|
||||
OCTANE_SERVER=frankenphp \
|
||||
TZ=${TZ} \
|
||||
USER=octane \
|
||||
ROOT=${APP_DIR} \
|
||||
APP_ENV=${APP_ENV} \
|
||||
COMPOSER_FUND=0 \
|
||||
COMPOSER_MAX_PARALLEL_HTTP=24 \
|
||||
XDG_CONFIG_HOME=${APP_DIR}/.config \
|
||||
XDG_DATA_HOME=${APP_DIR}/.data
|
||||
XDG_DATA_HOME=${APP_DIR}/.data \
|
||||
SERVER_NAME=${APP_HOST}
|
||||
|
||||
WORKDIR ${ROOT}
|
||||
|
||||
@@ -78,14 +65,16 @@ RUN apt-get update; \
|
||||
apt-get install -yqq --no-install-recommends --show-progress \
|
||||
apt-utils \
|
||||
curl \
|
||||
gcc \
|
||||
wget \
|
||||
nano \
|
||||
vim \
|
||||
git \
|
||||
ncdu \
|
||||
procps \
|
||||
unzip \
|
||||
ca-certificates \
|
||||
supervisor \
|
||||
libsodium-dev \
|
||||
libbrotli-dev \
|
||||
# Install PHP extensions (included with dunglas/frankenphp)
|
||||
&& install-php-extensions \
|
||||
bz2 \
|
||||
@@ -99,6 +88,8 @@ RUN apt-get update; \
|
||||
exif \
|
||||
pdo_mysql \
|
||||
zip \
|
||||
uv \
|
||||
vips \
|
||||
intl \
|
||||
gd \
|
||||
redis \
|
||||
@@ -128,27 +119,34 @@ RUN arch="$(uname -m)" \
|
||||
|
||||
RUN userdel --remove --force www-data \
|
||||
&& groupadd --force -g ${WWWGROUP} ${USER} \
|
||||
&& useradd -ms /bin/bash --no-log-init --no-user-group -g ${WWWGROUP} -u ${WWWUSER} ${USER}
|
||||
&& useradd -ms /bin/bash --no-log-init --no-user-group -g ${WWWGROUP} -u ${WWWUSER} ${USER} \
|
||||
&& setcap -r /usr/local/bin/frankenphp
|
||||
|
||||
RUN chown -R ${USER}:${USER} ${ROOT} /var/{log,run} \
|
||||
&& chmod -R a+rw ${ROOT} /var/{log,run}
|
||||
|
||||
RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini
|
||||
|
||||
COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/php.ini ${PHP_INI_DIR}/conf.d/99-octane-default.ini
|
||||
COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/php-arm.ini ${PHP_INI_DIR}/conf.d/99-octane-arm.ini
|
||||
|
||||
RUN echo "TARGETPLATFORM is equal to ${TARGETPLATFORM}"
|
||||
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
|
||||
rm ${PHP_INI_DIR}/conf.d/99-octane-default.ini; \
|
||||
else \
|
||||
rm ${PHP_INI_DIR}/conf.d/99-octane-arm.ini; \
|
||||
fi
|
||||
|
||||
USER ${USER}
|
||||
|
||||
COPY --chown=${USER}:${USER} --from=vendor /usr/bin/composer /usr/bin/composer
|
||||
#COPY --chown=${USER}:${USER} composer.json composer.lock ./
|
||||
COPY --link --chown=${WWWUSER}:${WWWUSER} --from=vendor /usr/bin/composer /usr/bin/composer
|
||||
|
||||
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.conf /etc/
|
||||
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/
|
||||
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.*.conf /etc/supervisor/conf.d/
|
||||
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/start-container /usr/local/bin/start-container
|
||||
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/healthcheck /usr/local/bin/healthcheck
|
||||
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/php.ini ${PHP_INI_DIR}/conf.d/99-octane.ini
|
||||
|
||||
RUN chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck
|
||||
|
||||
###########################################
|
||||
|
||||
#FROM base AS common
|
||||
#
|
||||
#USER ${USER}
|
||||
#
|
||||
#COPY --link --chown=${WWWUSER}:${WWWUSER} . .
|
||||
#
|
||||
#RUN composer install \
|
||||
# --no-dev \
|
||||
@@ -158,22 +156,47 @@ COPY --chown=${USER}:${USER} --from=vendor /usr/bin/composer /usr/bin/composer
|
||||
# --no-scripts \
|
||||
# --audit
|
||||
|
||||
COPY --chown=${USER}:${USER} . .
|
||||
#COPY --chown=${USER}:${USER} --from=build ${ROOT}/public public
|
||||
###########################################
|
||||
# Build frontend assets with Bun
|
||||
###########################################
|
||||
|
||||
#FROM oven/bun:${BUN_VERSION} AS build
|
||||
#
|
||||
#ARG APP_ENV
|
||||
#
|
||||
#ENV ROOT=/var/www/html \
|
||||
# APP_ENV=${APP_ENV} \
|
||||
# NODE_ENV=${APP_ENV:-production}
|
||||
#
|
||||
#WORKDIR ${ROOT}
|
||||
#
|
||||
#COPY --link package.json bun.lock* ./
|
||||
#
|
||||
#RUN bun install --frozen-lockfile
|
||||
#
|
||||
#COPY --link . .
|
||||
#COPY --link --from=common ${ROOT}/vendor vendor
|
||||
#
|
||||
#RUN bun run build
|
||||
|
||||
###########################################
|
||||
|
||||
#FROM common AS runner
|
||||
|
||||
USER ${USER}
|
||||
|
||||
ENV WITH_HORIZON=false \
|
||||
WITH_SCHEDULER=false \
|
||||
WITH_REVERB=false
|
||||
|
||||
COPY --link --chown=${WWWUSER}:${WWWUSER} . .
|
||||
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
|
||||
|
||||
RUN mkdir -p \
|
||||
storage/framework/{sessions,views,cache,testing} \
|
||||
storage/logs \
|
||||
bootstrap/cache && chmod -R a+rw storage
|
||||
|
||||
COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.conf /etc/supervisor/
|
||||
COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/
|
||||
COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.*.conf /etc/supervisor/conf.d/
|
||||
COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/start-container /usr/local/bin/start-container
|
||||
|
||||
# FrankenPHP embedded PHP configuration
|
||||
COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/php.ini /lib/php.ini
|
||||
|
||||
#RUN composer install \
|
||||
# --classmap-authoritative \
|
||||
# --no-interaction \
|
||||
@@ -183,12 +206,9 @@ COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/php.ini /lib/ph
|
||||
|
||||
RUN cat .env
|
||||
#RUN php artisan env
|
||||
RUN php artisan storage:link
|
||||
|
||||
RUN chmod +x /usr/local/bin/start-container
|
||||
|
||||
RUN cat ${DOCKER_FILES_BASE_PATH}deployment/utilities.sh >> ~/.bashrc
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["start-container"]
|
||||
|
||||
#HEALTHCHECK --start-period=5s --interval=2s --timeout=5s --retries=8 CMD healthcheck || exit 1
|
||||
|
||||
35
docker/prod/deployment/healthcheck
Normal file
35
docker/prod/deployment/healthcheck
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -e
|
||||
|
||||
container_mode=${CONTAINER_MODE:-"http"}
|
||||
|
||||
if [ "${container_mode}" = "http" ]; then
|
||||
php "${ROOT}/artisan" octane:status
|
||||
elif [ "${container_mode}" = "horizon" ]; then
|
||||
php "${ROOT}/artisan" horizon:status
|
||||
elif [ "${container_mode}" = "scheduler" ]; then
|
||||
if [ "$(supervisorctl status scheduler:scheduler_0 | awk '{print tolower($2)}')" = "running" ]; then
|
||||
exit 0
|
||||
else
|
||||
echo "Healthcheck failed."
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${container_mode}" = "reverb" ]; then
|
||||
if [ "$(supervisorctl status reverb:reverb_0 | awk '{print tolower($2)}')" = "running" ]; then
|
||||
exit 0
|
||||
else
|
||||
echo "Healthcheck failed."
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${container_mode}" = "worker" ]; then
|
||||
if [ "$(supervisorctl status worker:worker_0 | awk '{print tolower($2)}')" = "running" ]; then
|
||||
exit 0
|
||||
else
|
||||
echo "Healthcheck failed."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Container mode mismatched."
|
||||
exit 1
|
||||
fi
|
||||
68
docker/prod/deployment/octane/FrankenPHP/Caddyfile
Normal file
68
docker/prod/deployment/octane/FrankenPHP/Caddyfile
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
{$CADDY_GLOBAL_OPTIONS}
|
||||
|
||||
admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT}
|
||||
|
||||
frankenphp {
|
||||
worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
|
||||
}
|
||||
|
||||
metrics {
|
||||
per_host
|
||||
}
|
||||
|
||||
servers {
|
||||
protocols h1
|
||||
}
|
||||
}
|
||||
|
||||
{$CADDY_EXTRA_CONFIG}
|
||||
|
||||
{$CADDY_SERVER_SERVER_NAME} {
|
||||
log {
|
||||
level WARN
|
||||
|
||||
format filter {
|
||||
wrap {$CADDY_SERVER_LOGGER}
|
||||
fields {
|
||||
uri query {
|
||||
replace authorization REDACTED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route {
|
||||
root * "{$APP_PUBLIC_PATH}"
|
||||
encode zstd br gzip
|
||||
|
||||
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
||||
|
||||
request_body {
|
||||
max_size 500MB
|
||||
}
|
||||
|
||||
@static {
|
||||
file
|
||||
path *.js *.css *.jpg *.jpeg *.webp *.weba *.webm *.gif *.png *.ico *.cur *.gz *.svg *.svgz *.mp4 *.mp3 *.ogg *.ogv *.htc *.woff2 *.woff
|
||||
}
|
||||
|
||||
@staticshort {
|
||||
file
|
||||
path *.json *.xml *.rss
|
||||
}
|
||||
|
||||
header @static Cache-Control "public, immutable, stale-while-revalidate, max-age=31536000"
|
||||
|
||||
header @staticshort Cache-Control "no-cache, max-age=3600"
|
||||
|
||||
@rejected `path('*.bak', '*.conf', '*.dist', '*.fla', '*.ini', '*.inc', '*.inci', '*.log', '*.orig', '*.psd', '*.sh', '*.sql', '*.swo', '*.swp', '*.swop', '*/.*') && !path('*/.well-known/*')`
|
||||
error @rejected 401
|
||||
|
||||
php_server {
|
||||
index frankenphp-worker.php
|
||||
try_files {path} frankenphp-worker.php
|
||||
resolve_root_symlink
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,65 @@
|
||||
[program:octane]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan octane:start --server=frankenphp --host=0.0.0.0 --port=8000 --admin-port=2019
|
||||
; command=php %(ENV_ROOT)s/artisan octane:start --server=frankenphp --host=localhost --port=443 --admin-port=2019 --https --http-redirect
|
||||
user=%(ENV_USER)s
|
||||
autostart=true
|
||||
autorestart=true
|
||||
environment=LARAVEL_OCTANE="1"
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan octane:frankenphp --host=0.0.0.0 --port=8000 --admin-port=2019 --caddyfile=%(ENV_ROOT)s/docker/prod/deployment/octane/FrankenPHP/Caddyfile
|
||||
user = %(ENV_USER)s
|
||||
priority = 1
|
||||
autostart = true
|
||||
autorestart = true
|
||||
environment = LARAVEL_OCTANE = "1"
|
||||
stdout_logfile = /dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile = /dev/stderr
|
||||
stderr_logfile_maxbytes = 0
|
||||
|
||||
[program:horizon]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan horizon
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_HORIZON)s
|
||||
autorestart=true
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/horizon.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/horizon.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
stopwaitsecs=3600
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan horizon
|
||||
user = %(ENV_USER)s
|
||||
priority = 3
|
||||
autostart = %(ENV_WITH_HORIZON)s
|
||||
autorestart = true
|
||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
|
||||
stdout_logfile_maxbytes = 200MB
|
||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
|
||||
stderr_logfile_maxbytes = 200MB
|
||||
stopwaitsecs = 3600
|
||||
|
||||
[program:scheduler]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=supercronic -overlapping /etc/supercronic/laravel
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_SCHEDULER)s
|
||||
autorestart=true
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = supercronic -overlapping /etc/supercronic/laravel
|
||||
user = %(ENV_USER)s
|
||||
autostart = %(ENV_WITH_SCHEDULER)s
|
||||
autorestart = true
|
||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stdout_logfile_maxbytes = 200MB
|
||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stderr_logfile_maxbytes = 200MB
|
||||
|
||||
[program:clear-scheduler-cache]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_SCHEDULER)s
|
||||
autorestart=false
|
||||
startsecs=0
|
||||
startretries=1
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user = %(ENV_USER)s
|
||||
autostart = %(ENV_WITH_SCHEDULER)s
|
||||
autorestart = false
|
||||
startsecs = 0
|
||||
startretries = 1
|
||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stdout_logfile_maxbytes = 200MB
|
||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stderr_logfile_maxbytes = 200MB
|
||||
|
||||
[program:reverb]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan reverb:start
|
||||
user = %(ENV_USER)s
|
||||
priority = 2
|
||||
autostart = %(ENV_WITH_REVERB)s
|
||||
autorestart = true
|
||||
stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
|
||||
stdout_logfile_maxbytes = 200MB
|
||||
stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
|
||||
stderr_logfile_maxbytes = 200MB
|
||||
minfds = 10000
|
||||
|
||||
[include]
|
||||
files=/etc/supervisor/supervisord.conf
|
||||
files = /etc/supervisord.conf
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
version: '2.7'
|
||||
rpc:
|
||||
listen: 'tcp://127.0.0.1:6001'
|
||||
server:
|
||||
relay: pipes
|
||||
http:
|
||||
middleware: [ "static", "gzip", "headers" ]
|
||||
max_request_size: 20
|
||||
static:
|
||||
dir: "public"
|
||||
forbid: [ ".php", ".htaccess" ]
|
||||
uploads:
|
||||
forbid: [".php", ".exe", ".bat", ".sh"]
|
||||
pool:
|
||||
allocate_timeout: 10s
|
||||
destroy_timeout: 10s
|
||||
supervisor:
|
||||
max_worker_memory: 128
|
||||
exec_ttl: 60s
|
||||
logs:
|
||||
mode: production
|
||||
level: debug
|
||||
encoding: json
|
||||
status:
|
||||
address: localhost:2114
|
||||
@@ -1,50 +0,0 @@
|
||||
[program:octane]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan octane:start --server=roadrunner --host=0.0.0.0 --port=8000 --rpc-port=6001 --rr-config=%(ENV_ROOT)s/.rr.yaml
|
||||
user=%(ENV_USER)s
|
||||
autostart=true
|
||||
autorestart=true
|
||||
environment=LARAVEL_OCTANE="1"
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:horizon]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan horizon
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_HORIZON)s
|
||||
autorestart=true
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/horizon.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/horizon.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
stopwaitsecs=3600
|
||||
|
||||
[program:scheduler]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=supercronic -overlapping /etc/supercronic/laravel
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_SCHEDULER)s
|
||||
autorestart=true
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
|
||||
[program:clear-scheduler-cache]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_SCHEDULER)s
|
||||
autorestart=false
|
||||
startsecs=0
|
||||
startretries=1
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
|
||||
[include]
|
||||
files=/etc/supervisor/supervisord.conf
|
||||
@@ -1,50 +0,0 @@
|
||||
[program:octane]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000
|
||||
user=%(ENV_USER)s
|
||||
autostart=true
|
||||
autorestart=true
|
||||
environment=LARAVEL_OCTANE="1"
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:horizon]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan horizon
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_HORIZON)s
|
||||
autorestart=true
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/horizon.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/horizon.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
stopwaitsecs=3600
|
||||
|
||||
[program:scheduler]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=supercronic -overlapping /etc/supercronic/laravel
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_SCHEDULER)s
|
||||
autorestart=true
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
|
||||
[program:clear-scheduler-cache]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_SCHEDULER)s
|
||||
autorestart=false
|
||||
startsecs=0
|
||||
startretries=1
|
||||
stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stdout_logfile_maxbytes=200MB
|
||||
stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log
|
||||
stderr_logfile_maxbytes=200MB
|
||||
|
||||
[include]
|
||||
files=/etc/supervisor/supervisord.conf
|
||||
@@ -1,30 +0,0 @@
|
||||
[PHP]
|
||||
post_max_size = 100M
|
||||
upload_max_filesize = 100M
|
||||
expose_php = 0
|
||||
realpath_cache_size = 16M
|
||||
realpath_cache_ttl = 360
|
||||
max_input_time = 5
|
||||
|
||||
[Opcache]
|
||||
opcache.enable = 1
|
||||
opcache.enable_cli = 1
|
||||
opcache.memory_consumption = 256M
|
||||
opcache.use_cwd = 0
|
||||
opcache.max_file_size = 0
|
||||
opcache.max_accelerated_files = 32531
|
||||
opcache.validate_timestamps = 0
|
||||
opcache.file_update_protection = 0
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.file_cache = 60
|
||||
|
||||
[JIT]
|
||||
opcache.jit_buffer_size = 128M
|
||||
opcache.jit = disable
|
||||
opcache.jit_prof_threshold = 0.001
|
||||
opcache.jit_max_root_traces = 2048
|
||||
opcache.jit_max_side_traces = 256
|
||||
|
||||
[zlib]
|
||||
zlib.output_compression = On
|
||||
zlib.output_compression_level = 9
|
||||
@@ -5,6 +5,8 @@ expose_php = 0
|
||||
realpath_cache_size = 16M
|
||||
realpath_cache_ttl = 360
|
||||
max_input_time = 5
|
||||
register_argc_argv = 0
|
||||
date.timezone = ${TZ:-UTC}
|
||||
|
||||
[Opcache]
|
||||
opcache.enable = 1
|
||||
@@ -16,7 +18,6 @@ opcache.max_accelerated_files = 32531
|
||||
opcache.validate_timestamps = 0
|
||||
opcache.file_update_protection = 0
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.file_cache = 60
|
||||
|
||||
[JIT]
|
||||
opcache.jit_buffer_size = 128M
|
||||
|
||||
@@ -4,41 +4,49 @@ set -e
|
||||
container_mode=${CONTAINER_MODE:-"http"}
|
||||
octane_server=${OCTANE_SERVER}
|
||||
auto_db_migrate=${AUTO_DB_MIGRATE:-false}
|
||||
echo "Container mode: $container_mode"
|
||||
|
||||
initialStuff() {
|
||||
echo "Container mode: $container_mode"
|
||||
|
||||
if [ ${auto_db_migrate} = "true" ]; then
|
||||
echo "Auto database migration enabled."
|
||||
php artisan migrate --isolated --force
|
||||
fi
|
||||
|
||||
php artisan storage:link; \
|
||||
php artisan optimize:clear; \
|
||||
php artisan event:cache; \
|
||||
php artisan config:cache; \
|
||||
php artisan route:cache;
|
||||
php artisan optimize;
|
||||
}
|
||||
|
||||
if [ "$1" != "" ]; then
|
||||
exec "$@"
|
||||
elif [ ${container_mode} = "http" ]; then
|
||||
echo "Octane Server: $octane_server"
|
||||
elif [ "${container_mode}" = "http" ]; then
|
||||
initialStuff
|
||||
if [ ${octane_server} = "frankenphp" ]; then
|
||||
echo "Octane Server: $octane_server"
|
||||
if [ "${octane_server}" = "frankenphp" ]; then
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.frankenphp.conf
|
||||
elif [ ${octane_server} = "swoole" ]; then
|
||||
elif [ "${octane_server}" = "swoole" ]; then
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.swoole.conf
|
||||
elif [ ${octane_server} = "roadrunner" ]; then
|
||||
elif [ "${octane_server}" = "roadrunner" ]; then
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.roadrunner.conf
|
||||
else
|
||||
echo "Invalid Octane server supplied."
|
||||
exit 1
|
||||
fi
|
||||
elif [ ${container_mode} = "horizon" ]; then
|
||||
elif [ "${container_mode}" = "horizon" ]; then
|
||||
initialStuff
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.horizon.conf
|
||||
elif [ ${container_mode} = "scheduler" ]; then
|
||||
elif [ "${container_mode}" = "reverb" ]; then
|
||||
initialStuff
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.reverb.conf
|
||||
elif [ "${container_mode}" = "scheduler" ]; then
|
||||
initialStuff
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.scheduler.conf
|
||||
elif [ ${container_mode} = "worker" ]; then
|
||||
elif [ "${container_mode}" = "worker" ]; then
|
||||
if [ -z "${WORKER_COMMAND}" ]; then
|
||||
echo "WORKER_COMMAND is undefined."
|
||||
exit 1
|
||||
fi
|
||||
initialStuff
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.worker.conf
|
||||
else
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=%(ENV_USER)s
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
|
||||
[unix_http_server]
|
||||
file=/var/run/supervisor.sock
|
||||
nodaemon = true
|
||||
user = %(ENV_USER)s
|
||||
logfile = /var/log/supervisor/supervisord.log
|
||||
pidfile = /var/run/supervisord.pid
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///var/run/supervisor.sock
|
||||
|
||||
[inet_http_server]
|
||||
port = 127.0.0.1:9001
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
[program:horizon]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan horizon
|
||||
user=%(ENV_USER)s
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
stopwaitsecs=3600
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan horizon
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stdout_logfile = /dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile = /dev/stderr
|
||||
stderr_logfile_maxbytes = 0
|
||||
stopwaitsecs = 3600
|
||||
|
||||
[include]
|
||||
files=/etc/supervisor/supervisord.conf
|
||||
files = /etc/supervisord.conf
|
||||
|
||||
14
docker/prod/deployment/supervisord.reverb.conf
Normal file
14
docker/prod/deployment/supervisord.reverb.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
[program:reverb]
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan reverb:start
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stdout_logfile = /dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile = /dev/stderr
|
||||
stderr_logfile_maxbytes = 0
|
||||
minfds = 10000
|
||||
|
||||
[include]
|
||||
files = /etc/supervisord.conf
|
||||
@@ -1,26 +1,26 @@
|
||||
[program:scheduler]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=supercronic -overlapping /etc/supercronic/laravel
|
||||
user=%(ENV_USER)s
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = supercronic -overlapping /etc/supercronic/laravel
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stdout_logfile = /dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile = /dev/stderr
|
||||
stderr_logfile_maxbytes = 0
|
||||
|
||||
[program:clear-scheduler-cache]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user=%(ENV_USER)s
|
||||
autostart=true
|
||||
autorestart=false
|
||||
startsecs=0
|
||||
startretries=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = false
|
||||
startsecs = 0
|
||||
startretries = 1
|
||||
stdout_logfile = /dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile = /dev/stderr
|
||||
stderr_logfile_maxbytes = 0
|
||||
|
||||
[include]
|
||||
files=/etc/supervisor/supervisord.conf
|
||||
files = /etc/supervisord.conf
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=%(ENV_USER)s
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
|
||||
[program:octane]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000
|
||||
user=%(ENV_USER)s
|
||||
autostart=true
|
||||
autorestart=true
|
||||
environment=LARAVEL_OCTANE="1"
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:horizon]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan horizon
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_HORIZON)s
|
||||
autorestart=true
|
||||
stdout_logfile=%(ENV_ROOT)s/horizon.log
|
||||
stopwaitsecs=3600
|
||||
|
||||
[program:scheduler]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=supercronic -overlapping /etc/supercronic/laravel
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_SCHEDULER)s
|
||||
autorestart=true
|
||||
stdout_logfile=%(ENV_ROOT)s/scheduler.log
|
||||
|
||||
[program:clear-scheduler-cache]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php %(ENV_ROOT)s/artisan schedule:clear-cache
|
||||
user=%(ENV_USER)s
|
||||
autostart=%(ENV_WITH_SCHEDULER)s
|
||||
autorestart=false
|
||||
stdout_logfile=%(ENV_ROOT)s/scheduler.log
|
||||
@@ -1,13 +1,13 @@
|
||||
[program:worker]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=%(ENV_WORKER_COMMAND)s
|
||||
user=%(ENV_USER)s
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
process_name = %(program_name)s_%(process_num)s
|
||||
command = %(ENV_WORKER_COMMAND)s
|
||||
user = %(ENV_USER)s
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stdout_logfile = /dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile = /dev/stderr
|
||||
stderr_logfile_maxbytes = 0
|
||||
|
||||
[include]
|
||||
files=/etc/supervisor/supervisord.conf
|
||||
files = /etc/supervisord.conf
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
tinker() {
|
||||
if [ -z "$1" ]; then
|
||||
php artisan tinker
|
||||
else
|
||||
php artisan tinker --execute="\"dd($1);\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Commonly used aliases
|
||||
alias ..="cd .."
|
||||
alias ...="cd ../.."
|
||||
alias art="php artisan"
|
||||
@@ -12,3 +12,7 @@ parameters:
|
||||
checkOctaneCompatibility: true
|
||||
checkModelProperties: true
|
||||
noEnvCallsOutsideOfConfig: true
|
||||
|
||||
ignoreErrors:
|
||||
- '# is not subtype of native type Illuminate\\Database\\Eloquent\\Builder#'
|
||||
- '# is not subtype of native type Illuminate\\Database\\Eloquent\\Relations\\Relation#'
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
@component('mail::message')
|
||||
|
||||
{{ __('The API token ":token" expired.', ['token' => $tokenName]) }}
|
||||
|
||||
|
||||
{{ __('You can create a new API token in your profile:') }}
|
||||
|
||||
@component('mail::button', ['url' => $profileUrl])
|
||||
{{ __('Go to your profile') }}
|
||||
@endcomponent
|
||||
|
||||
@endcomponent
|
||||
13
resources/views/emails/auth-api-token-expired.blade.php
Normal file
13
resources/views/emails/auth-api-token-expired.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@component('mail::message')
|
||||
|
||||
{{ __('The API token ":token" will expire in 7 days!', ['token' => $tokenName]) }}
|
||||
|
||||
{{ __('Please make sure to create a new API token and use the new one instead before it expires to avoid any disruptions in service.') }}
|
||||
|
||||
{{ __('You can create a new API token in your profile:') }}
|
||||
|
||||
@component('mail::button', ['url' => $profileUrl])
|
||||
{{ __('Go to your profile') }}
|
||||
@endcomponent
|
||||
|
||||
@endcomponent
|
||||
@@ -114,6 +114,7 @@ Route::prefix('v1')->name('v1.')->group(static function (): void {
|
||||
|
||||
Route::name('users.time-entries.')->group(static function (): void {
|
||||
Route::get('/users/me/time-entries/active', [UserTimeEntryController::class, 'myActive'])->name('my-active');
|
||||
Route::get('/users/me/time-entries', [UserTimeEntryController::class, 'my'])->name('my'); // TODO
|
||||
});
|
||||
|
||||
// Report routes
|
||||
|
||||
@@ -10,11 +10,9 @@ use App\Service\DeletionService;
|
||||
use Illuminate\Support\Str;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(OrganizationDeleteCommand::class)]
|
||||
#[UsesClass(OrganizationDeleteCommand::class)]
|
||||
class OrganizationDeleteCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_it_calls_the_deletion_service_with_the_organization(): void
|
||||
|
||||
@@ -10,11 +10,9 @@ use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(UserCreateCommand::class)]
|
||||
#[UsesClass(UserCreateCommand::class)]
|
||||
class UserCreateCommandCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_it_creates_user(): void
|
||||
|
||||
@@ -9,11 +9,9 @@ use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(UserVerifyCommand::class)]
|
||||
#[UsesClass(UserVerifyCommand::class)]
|
||||
class UserVerifyCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_it_verifies_user_email(): void
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Console\Commands\Auth;
|
||||
|
||||
use App\Console\Commands\Auth\AuthSendReminderForExpiringApiTokensCommand;
|
||||
use App\Mail\AuthApiTokenExpirationReminderMail;
|
||||
use App\Mail\AuthApiTokenExpiredMail;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(AuthSendReminderForExpiringApiTokensCommand::class)]
|
||||
class AuthSendReminderForExpiringApiTokensCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_sends_mail_for_expired_api_tokens_but_ignores_the_one_where_the_mail_was_already_sent_and_ignores_non_api_tokens(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$apiClient = Client::factory()->apiClient()->create();
|
||||
$otherClient = Client::factory()->desktopClient()->create();
|
||||
$expiredToken = Token::factory()->forUser($user->user)->forClient($apiClient)->create([
|
||||
'reminder_sent_at' => Carbon::now()->subDays(8),
|
||||
'expired_info_sent_at' => null,
|
||||
'expires_at' => Carbon::now()->subDay(),
|
||||
]);
|
||||
$expiredTokenWithMailSent = Token::factory()->forUser($user->user)->forClient($apiClient)->create([
|
||||
'reminder_sent_at' => Carbon::now()->subDays(8),
|
||||
'expired_info_sent_at' => Carbon::now(),
|
||||
'expires_at' => Carbon::now()->subDay(),
|
||||
]);
|
||||
$nonApiToken = Token::factory()->forUser($user->user)->forClient($otherClient)->create([
|
||||
'reminder_sent_at' => null,
|
||||
'expired_info_sent_at' => null,
|
||||
'expires_at' => Carbon::now()->subDay(),
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('auth:send-mails-expiring-api-tokens');
|
||||
|
||||
// Assert
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$expiredToken->refresh();
|
||||
$expiredTokenWithMailSent->refresh();
|
||||
$nonApiToken->refresh();
|
||||
$this->assertNotNull($expiredToken->expired_info_sent_at);
|
||||
$this->assertNotNull($expiredTokenWithMailSent->expired_info_sent_at);
|
||||
$this->assertNull($nonApiToken->reminder_sent_at);
|
||||
$this->assertNull($nonApiToken->expired_info_sent_at);
|
||||
Mail::assertNotQueued(AuthApiTokenExpirationReminderMail::class);
|
||||
Mail::assertQueued(AuthApiTokenExpiredMail::class, function (AuthApiTokenExpiredMail $mail) use ($user, $expiredToken): bool {
|
||||
return $mail->hasTo($user->user->email) &&
|
||||
$mail->token->is($expiredToken) &&
|
||||
$mail->user->is($user->user);
|
||||
});
|
||||
|
||||
$output = Artisan::output();
|
||||
$this->assertStringContainsString('Finished sending 0 expiring API token emails...', $output);
|
||||
$this->assertStringContainsString('Finished sending 1 expired API token emails...', $output);
|
||||
$this->assertStringContainsString(
|
||||
'Start sending email to user "'.$user->user->email.'" ('.
|
||||
$user->user->id.') about expired API token '.$expiredToken->getKey(), $output);
|
||||
}
|
||||
|
||||
public function test_sends_mail_for_api_tokens_that_expire_soon_but_ignores_the_one_where_the_mail_was_already_sent_and_ignores_non_api_tokens(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$apiClient = Client::factory()->apiClient()->create();
|
||||
$otherClient = Client::factory()->desktopClient()->create();
|
||||
$expiringToken = Token::factory()->forUser($user->user)->forClient($apiClient)->create([
|
||||
'reminder_sent_at' => null,
|
||||
'expired_info_sent_at' => null,
|
||||
'expires_at' => Carbon::now()->addDays(6),
|
||||
]);
|
||||
$expiringTokenWithMailSent = Token::factory()->forUser($user->user)->forClient($apiClient)->create([
|
||||
'reminder_sent_at' => Carbon::now(),
|
||||
'expired_info_sent_at' => null,
|
||||
'expires_at' => Carbon::now()->addDays(6),
|
||||
]);
|
||||
$nonApiToken = Token::factory()->forUser($user->user)->forClient($otherClient)->create([
|
||||
'reminder_sent_at' => null,
|
||||
'expired_info_sent_at' => null,
|
||||
'expires_at' => Carbon::now()->addDays(6),
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('auth:send-mails-expiring-api-tokens');
|
||||
|
||||
// Assert
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$expiringToken->refresh();
|
||||
$expiringTokenWithMailSent->refresh();
|
||||
$nonApiToken->refresh();
|
||||
$this->assertNotNull($expiringToken->reminder_sent_at);
|
||||
$this->assertNull($expiringToken->expired_info_sent_at);
|
||||
$this->assertNotNull($expiringTokenWithMailSent->reminder_sent_at);
|
||||
$this->assertNull($expiringTokenWithMailSent->expired_info_sent_at);
|
||||
$this->assertNull($nonApiToken->reminder_sent_at);
|
||||
$this->assertNull($nonApiToken->expired_info_sent_at);
|
||||
Mail::assertNotQueued(AuthApiTokenExpiredMail::class);
|
||||
Mail::assertQueued(AuthApiTokenExpirationReminderMail::class, function (AuthApiTokenExpirationReminderMail $mail) use ($user, $expiringToken): bool {
|
||||
return $mail->hasTo($user->user->email) &&
|
||||
$mail->token->is($expiringToken) &&
|
||||
$mail->user->is($user->user);
|
||||
});
|
||||
|
||||
$output = Artisan::output();
|
||||
$this->assertStringContainsString('Finished sending 1 expiring API token emails...', $output);
|
||||
$this->assertStringContainsString('Finished sending 0 expired API token emails...', $output);
|
||||
$this->assertStringContainsString(
|
||||
'Start sending email to user "'.$user->user->email.'" ('.
|
||||
$user->user->id.') reminding about API token '.$expiringToken->getKey(), $output);
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,9 @@ use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(CorrectionPlaceholderMembersCommand::class)]
|
||||
#[UsesClass(CorrectionPlaceholderMembersCommand::class)]
|
||||
class CorrectionPlaceholderMembersCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_sets_member_role_to_placeholder_if_user_is_placeholder(): void
|
||||
|
||||
@@ -9,11 +9,9 @@ use App\Models\Report;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(ReportSetExpiredToPrivateCommand::class)]
|
||||
#[UsesClass(ReportSetExpiredToPrivateCommand::class)]
|
||||
class ReportSetExpiredToPrivateCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_command_sets_expired_reports_to_private(): void
|
||||
|
||||
@@ -12,12 +12,10 @@ use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
#[CoversClass(SelfHostCheckForUpdateCommand::class)]
|
||||
#[CoversClass(ApiService::class)]
|
||||
#[UsesClass(SelfHostCheckForUpdateCommand::class)]
|
||||
class SelfHostCheckForUpdateCommandTest extends TestCase
|
||||
{
|
||||
public function test_checks_for_update_and_saves_version_in_cache(): void
|
||||
|
||||
@@ -15,11 +15,9 @@ use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(SelfHostDatabaseConsistency::class)]
|
||||
#[UsesClass(SelfHostDatabaseConsistency::class)]
|
||||
class SelfHostDatabaseConsistencyCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_checks_that_task_need_to_be_part_of_project_in_time_entries(): void
|
||||
|
||||
@@ -8,11 +8,9 @@ use App\Console\Commands\SelfHost\SelfHostGenerateKeysCommand;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
#[CoversClass(SelfHostGenerateKeysCommand::class)]
|
||||
#[UsesClass(SelfHostGenerateKeysCommand::class)]
|
||||
class SelfHostGenerateKeysCommandTest extends TestCase
|
||||
{
|
||||
public function test_generates_app_key_and_passport_keys_per_default_in_env_format(): void
|
||||
|
||||
@@ -11,12 +11,10 @@ use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
#[CoversClass(SelfHostTelemetryCommand::class)]
|
||||
#[CoversClass(ApiService::class)]
|
||||
#[UsesClass(SelfHostTelemetryCommand::class)]
|
||||
class SelfHostTelemetryCommandTest extends TestCase
|
||||
{
|
||||
public function test_telemetry_sends_data_to_telemetry_endpoint_of_solidtime_cloud(): void
|
||||
|
||||
@@ -12,11 +12,9 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(TimeEntrySendStillRunningMailsCommand::class)]
|
||||
#[UsesClass(TimeEntrySendStillRunningMailsCommand::class)]
|
||||
class TimeEntrySendStillRunningMailsCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_sends_mails_for_still_running_time_entries(): void
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace Tests\Unit\Endpoint\Api\V1;
|
||||
use App\Http\Controllers\Api\V1\ApiTokenController;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Laravel\Passport\ClientRepository;
|
||||
use Laravel\Passport\Passport;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
@@ -20,8 +19,6 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$personalAccessClient = $this->createPersonalAccessClient();
|
||||
Config::set('passport.personal_access_client.id', $personalAccessClient->id);
|
||||
Config::set('passport.personal_access_client.secret', $personalAccessClient->secret);
|
||||
$client = $this->createClient();
|
||||
$token = Token::factory()->forUser($data->user)->forClient($personalAccessClient)->create();
|
||||
$otherTokenType = Token::factory()->forUser($data->user)->forClient($client)->create();
|
||||
@@ -34,6 +31,7 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
|
||||
// Assert
|
||||
$this->assertResponseCode($response, 200);
|
||||
$response->assertJsonCount(1, 'data');
|
||||
$response->assertExactJson([
|
||||
'data' => [
|
||||
[
|
||||
@@ -53,8 +51,6 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$personalAccessClient = $this->createPersonalAccessClient();
|
||||
Config::set('passport.personal_access_client.id', $personalAccessClient->id);
|
||||
Config::set('passport.personal_access_client.secret', $personalAccessClient->secret);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
@@ -102,8 +98,6 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$client = $this->createPersonalAccessClient();
|
||||
Config::set('passport.personal_access_client.id', $client->id);
|
||||
Config::set('passport.personal_access_client.secret', $client->secret);
|
||||
$token = Token::factory()->forUser($data->user)->forClient($client)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
@@ -123,8 +117,6 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$personalAccessClient = $this->createPersonalAccessClient();
|
||||
Config::set('passport.personal_access_client.id', $personalAccessClient->id);
|
||||
Config::set('passport.personal_access_client.secret', $personalAccessClient->secret);
|
||||
$client = $this->createClient();
|
||||
$token = Token::factory()->forUser($data->user)->forClient($client)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -153,34 +145,12 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
$this->assertResponseCode($response, 404);
|
||||
}
|
||||
|
||||
public function test_revoke_fails_if_personal_access_client_is_not_configured(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$client = $this->createPersonalAccessClient();
|
||||
$token = Token::factory()->forUser($data->user)->forClient($client)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.api-tokens.revoke', $token->id));
|
||||
|
||||
// Assert
|
||||
$this->assertResponseCode($response, 400);
|
||||
$response->assertExactJson([
|
||||
'error' => true,
|
||||
'key' => 'personal_access_client_is_not_configured',
|
||||
'message' => 'Personal access client is not configured',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_revoke_fails_if_the_token_does_not_belong_to_the_user(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$otherData = $this->createUserWithPermission([]);
|
||||
$client = $this->createPersonalAccessClient();
|
||||
Config::set('passport.personal_access_client.id', $client->id);
|
||||
Config::set('passport.personal_access_client.secret', $client->secret);
|
||||
$token = Token::factory()->forUser($otherData->user)->forClient($client)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
@@ -200,8 +170,6 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$client = $this->createPersonalAccessClient();
|
||||
Config::set('passport.personal_access_client.id', $client->id);
|
||||
Config::set('passport.personal_access_client.secret', $client->secret);
|
||||
$token = Token::factory()->forUser($data->user)->forClient($client)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
@@ -213,33 +181,11 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
$this->assertDatabaseMissing(Token::class, ['id' => $token->id]);
|
||||
}
|
||||
|
||||
public function test_destroy_fails_if_personal_access_client_is_not_configured(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$client = $this->createPersonalAccessClient();
|
||||
$token = Token::factory()->forUser($data->user)->forClient($client)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.api-tokens.destroy', $token->id));
|
||||
|
||||
// Assert
|
||||
$this->assertResponseCode($response, 400);
|
||||
$response->assertExactJson([
|
||||
'error' => true,
|
||||
'key' => 'personal_access_client_is_not_configured',
|
||||
'message' => 'Personal access client is not configured',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_fails_if_token_is_not_personal_access_token(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$personalAccessClient = $this->createPersonalAccessClient();
|
||||
Config::set('passport.personal_access_client.id', $personalAccessClient->id);
|
||||
Config::set('passport.personal_access_client.secret', $personalAccessClient->secret);
|
||||
$client = $this->createClient();
|
||||
$token = Token::factory()->forUser($data->user)->forClient($client)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -273,8 +219,6 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
$data = $this->createUserWithPermission([]);
|
||||
$otherData = $this->createUserWithPermission([]);
|
||||
$client = $this->createPersonalAccessClient();
|
||||
Config::set('passport.personal_access_client.id', $client->id);
|
||||
Config::set('passport.personal_access_client.secret', $client->secret);
|
||||
$token = Token::factory()->forUser($otherData->user)->forClient($client)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
@@ -292,9 +236,7 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
$clientRepository = new ClientRepository;
|
||||
/** @var Client $client */
|
||||
$client = $clientRepository->createPersonalAccessClient(
|
||||
null, 'Test Personal Access Client', 'http://localhost'
|
||||
);
|
||||
$client = $clientRepository->createPersonalAccessGrantClient('Test Personal Access Client');
|
||||
|
||||
return $client;
|
||||
}
|
||||
@@ -303,8 +245,9 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
$clientRepository = new ClientRepository;
|
||||
/** @var Client $client */
|
||||
$client = $clientRepository->create(
|
||||
null, 'Desktop App', 'http://localhost', null
|
||||
$client = $clientRepository->createAuthorizationCodeGrantClient(
|
||||
name: 'Desktop App',
|
||||
redirectUris: ['http://localhost']
|
||||
);
|
||||
|
||||
return $client;
|
||||
|
||||
@@ -7,11 +7,9 @@ namespace Tests\Unit\Endpoint\Api\V1;
|
||||
use App\Http\Controllers\Api\V1\CurrencyController;
|
||||
use App\Service\CurrencyService;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(CurrencyController::class)]
|
||||
#[CoversClass(CurrencyService::class)]
|
||||
#[UsesClass(CurrencyController::class)]
|
||||
class CurrencyEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
public function test_index_return_list_of_available_currencies_incl_symbol(): void
|
||||
|
||||
@@ -10,10 +10,8 @@ use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(DashboardController::class)]
|
||||
#[UsesClass(DashboardController::class)]
|
||||
class DashboardEndpointTest extends EndpointTestAbstract
|
||||
{
|
||||
public function test_showing_dashboard_succeeds_for_empty_user(): void
|
||||
|
||||
@@ -7,10 +7,8 @@ namespace Tests\Unit\Endpoint\Web;
|
||||
use App\Http\Controllers\Web\HealthCheckController;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(HealthCheckController::class)]
|
||||
#[UsesClass(HealthCheckController::class)]
|
||||
class HealthCheckEndpointTest extends EndpointTestAbstract
|
||||
{
|
||||
public function test_up_endpoint_returns_ok(): void
|
||||
|
||||
@@ -9,12 +9,10 @@ use App\Http\Middleware\Authenticate;
|
||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||
use App\Models\User;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(HomeController::class)]
|
||||
#[CoversClass(Authenticate::class)]
|
||||
#[CoversClass(RedirectIfAuthenticated::class)]
|
||||
#[UsesClass(HomeController::class)]
|
||||
class HomeEndpointTest extends EndpointTestAbstract
|
||||
{
|
||||
public function test_index_redirects_to_dashboard_if_user_is_logged_in(): void
|
||||
|
||||
33
tests/Unit/Mail/AuthApiTokenExpirationReminderMailTest.php
Normal file
33
tests/Unit/Mail/AuthApiTokenExpirationReminderMailTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Mail;
|
||||
|
||||
use App\Mail\AuthApiTokenExpirationReminderMail;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use App\Models\User;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(AuthApiTokenExpirationReminderMail::class)]
|
||||
class AuthApiTokenExpirationReminderMailTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_mail_renders_content_correctly(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->create();
|
||||
$client = Client::factory()->apiClient()->create();
|
||||
$token = Token::factory()->forClient($client)->forUser($user)->create([
|
||||
'name' => 'TEST',
|
||||
]);
|
||||
$mail = new AuthApiTokenExpirationReminderMail($token, $user);
|
||||
|
||||
// Act
|
||||
$rendered = $mail->render();
|
||||
|
||||
// Assert
|
||||
$this->assertStringContainsString('The API token "TEST" expired.', $rendered);
|
||||
}
|
||||
}
|
||||
33
tests/Unit/Mail/AuthApiTokenExpiredMailTest.php
Normal file
33
tests/Unit/Mail/AuthApiTokenExpiredMailTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Mail;
|
||||
|
||||
use App\Mail\AuthApiTokenExpiredMail;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use App\Models\User;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(AuthApiTokenExpiredMail::class)]
|
||||
class AuthApiTokenExpiredMailTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_mail_renders_content_correctly(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->create();
|
||||
$client = Client::factory()->apiClient()->create();
|
||||
$token = Token::factory()->forClient($client)->forUser($user)->create([
|
||||
'name' => 'TEST',
|
||||
]);
|
||||
$mail = new AuthApiTokenExpiredMail($token, $user);
|
||||
|
||||
// Act
|
||||
$rendered = $mail->render();
|
||||
|
||||
// Assert
|
||||
$this->assertStringContainsString('The API token "TEST" will expire in 7 days!', $rendered);
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,9 @@ use App\Mail\OrganizationInvitationMail;
|
||||
use App\Models\Organization;
|
||||
use App\Models\OrganizationInvitation;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(OrganizationInvitationMail::class)]
|
||||
#[UsesClass(OrganizationInvitationMail::class)]
|
||||
class OrganizationInvitationMailTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_mail_renders_content_correctly(): void
|
||||
|
||||
@@ -7,11 +7,9 @@ namespace Tests\Unit\Mail;
|
||||
use App\Mail\TimeEntryStillRunningMail;
|
||||
use App\Models\TimeEntry;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(TimeEntryStillRunningMail::class)]
|
||||
#[UsesClass(TimeEntryStillRunningMail::class)]
|
||||
class TimeEntryStillRunningMailTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_mail_renders_content_correctly(): void
|
||||
|
||||
@@ -14,10 +14,8 @@ use Illuminate\Support\Str;
|
||||
use Laravel\Passport\Passport;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(CheckOrganizationBlocked::class)]
|
||||
#[UsesClass(CheckOrganizationBlocked::class)]
|
||||
class CheckOrganizationBlockedMiddlewareTest extends MiddlewareTestAbstract
|
||||
{
|
||||
private function createTestRoute(): void
|
||||
|
||||
@@ -8,10 +8,8 @@ use App\Http\Middleware\EnsureEmailIsVerified;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(EnsureEmailIsVerified::class)]
|
||||
#[UsesClass(EnsureEmailIsVerified::class)]
|
||||
class EnsureEmailIsVerifiedMiddlewareTest extends MiddlewareTestAbstract
|
||||
{
|
||||
private function createTestRoute(): string
|
||||
|
||||
@@ -8,10 +8,8 @@ use App\Http\Middleware\ForceHttps;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(ForceHttps::class)]
|
||||
#[UsesClass(ForceHttps::class)]
|
||||
class ForceHttpsMiddlewareTest extends MiddlewareTestAbstract
|
||||
{
|
||||
private function createTestRoute(): string
|
||||
|
||||
@@ -14,10 +14,8 @@ use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Laravel\Passport\Passport;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(HandleInertiaRequests::class)]
|
||||
#[UsesClass(HandleInertiaRequests::class)]
|
||||
class HandleInertiaRequestsMiddlewareTest extends MiddlewareTestAbstract
|
||||
{
|
||||
private function createTestRoute(): string
|
||||
|
||||
@@ -8,10 +8,8 @@ use App\Models\Client;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(Client::class)]
|
||||
#[UsesClass(Client::class)]
|
||||
class ClientModelTest extends ModelTestAbstract
|
||||
{
|
||||
public function test_it_belongs_to_a_organization(): void
|
||||
|
||||
@@ -10,10 +10,8 @@ use App\Models\Project;
|
||||
use App\Models\ProjectMember;
|
||||
use App\Models\User;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(Member::class)]
|
||||
#[UsesClass(Member::class)]
|
||||
class MemberModelTest extends ModelTestAbstract
|
||||
{
|
||||
public function test_it_belongs_to_a_user(): void
|
||||
|
||||
@@ -7,10 +7,8 @@ namespace Tests\Unit\Model;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(Organization::class)]
|
||||
#[UsesClass(Organization::class)]
|
||||
class OrganizationModelTest extends ModelTestAbstract
|
||||
{
|
||||
public function test_it_has_many_members(): void
|
||||
|
||||
100
tests/Unit/Model/Passport/TokenModelTest.php
Normal file
100
tests/Unit/Model/Passport/TokenModelTest.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Model\Passport;
|
||||
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use App\Models\User;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\Unit\Model\ModelTestAbstract;
|
||||
|
||||
#[CoversClass(Token::class)]
|
||||
class TokenModelTest extends ModelTestAbstract
|
||||
{
|
||||
public function test_it_belongs_to_a_client(): void
|
||||
{
|
||||
// Arrange
|
||||
$client = Client::factory()->create();
|
||||
$token = Token::factory()->forClient($client)->create();
|
||||
|
||||
// Act
|
||||
$token->refresh();
|
||||
$clientRel = $token->client;
|
||||
|
||||
// Assert
|
||||
$this->assertNotNull($clientRel);
|
||||
$this->assertTrue($clientRel->is($client));
|
||||
}
|
||||
|
||||
public function test_it_belongs_to_a_user(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->create();
|
||||
$client = Client::factory()->create();
|
||||
$token = Token::factory()->forUser($user)->forClient($client)->create();
|
||||
|
||||
// Act
|
||||
$token->refresh();
|
||||
$userRel = $token->user;
|
||||
|
||||
// Assert
|
||||
$this->assertNotNull($userRel);
|
||||
$this->assertTrue($userRel->is($user));
|
||||
}
|
||||
|
||||
public function test_scope_is_api_tokens_only_returns_api_tokens_with_no_parameters(): void
|
||||
{
|
||||
// Arrange
|
||||
$clientApi = Client::factory()->apiClient()->create();
|
||||
$clientDesktop = Client::factory()->desktopClient()->create();
|
||||
$token1 = Token::factory()->forClient($clientApi)->create();
|
||||
$token2 = Token::factory()->forClient($clientDesktop)->create();
|
||||
|
||||
// Act
|
||||
$apiTokens = Token::query()
|
||||
->isApiToken()
|
||||
->get();
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $apiTokens);
|
||||
$this->assertTrue($apiTokens->first()->is($token1));
|
||||
}
|
||||
|
||||
public function test_scope_is_api_tokens_only_returns_api_tokens_with_true(): void
|
||||
{
|
||||
// Arrange
|
||||
$clientApi = Client::factory()->apiClient()->create();
|
||||
$clientDesktop = Client::factory()->desktopClient()->create();
|
||||
$token1 = Token::factory()->forClient($clientApi)->create();
|
||||
$token2 = Token::factory()->forClient($clientDesktop)->create();
|
||||
|
||||
// Act
|
||||
$apiTokens = Token::query()
|
||||
->isApiToken(true)
|
||||
->get();
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $apiTokens);
|
||||
$this->assertTrue($apiTokens->first()->is($token1));
|
||||
}
|
||||
|
||||
public function test_scope_is_api_tokens_only_returns_api_tokens_with_false(): void
|
||||
{
|
||||
// Arrange
|
||||
$clientApi = Client::factory()->apiClient()->create();
|
||||
$clientDesktop = Client::factory()->desktopClient()->create();
|
||||
$token1 = Token::factory()->forClient($clientApi)->create();
|
||||
$token2 = Token::factory()->forClient($clientDesktop)->create();
|
||||
|
||||
// Act
|
||||
$apiTokens = Token::query()
|
||||
->isApiToken(false)
|
||||
->get();
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $apiTokens);
|
||||
$this->assertTrue($apiTokens->first()->is($token2));
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,8 @@ use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectMember;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
|
||||
#[CoversClass(ProjectMember::class)]
|
||||
#[UsesClass(ProjectMember::class)]
|
||||
class ProjectMemberModelTest extends ModelTestAbstract
|
||||
{
|
||||
public function test_it_belongs_to_a_project(): void
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user