mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-10 17:22:15 +01:00
Compare commits
1 Commits
v0.5.0
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45a60a926f |
1
.env.ci
1
.env.ci
@@ -5,6 +5,7 @@ APP_DEBUG=true
|
|||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
APP_FORCE_HTTPS=false
|
APP_FORCE_HTTPS=false
|
||||||
APP_ENABLE_REGISTRATION=true
|
APP_ENABLE_REGISTRATION=true
|
||||||
|
SESSION_SECURE_COOKIE=false
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ VITE_APP_NAME=solidtime
|
|||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
APP_DEBUG=false
|
APP_DEBUG=false
|
||||||
APP_FORCE_HTTPS=true
|
APP_FORCE_HTTPS=true
|
||||||
|
SESSION_SECURE_COOKIE=true
|
||||||
OCTANE_SERVER=frankenphp
|
OCTANE_SERVER=frankenphp
|
||||||
PAGINATION_PER_PAGE_DEFAULT=500
|
PAGINATION_PER_PAGE_DEFAULT=500
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class UserResource extends Resource
|
|||||||
{
|
{
|
||||||
/** @var User|null $record */
|
/** @var User|null $record */
|
||||||
$record = $form->getRecord();
|
$record = $form->getRecord();
|
||||||
|
|
||||||
return $form
|
return $form
|
||||||
->columns(1)
|
->columns(1)
|
||||||
->schema([
|
->schema([
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ class HealthCheckController extends Controller
|
|||||||
$response['app_env'] = app()->environment();
|
$response['app_env'] = app()->environment();
|
||||||
$response['app_timezone'] = config('app.timezone');
|
$response['app_timezone'] = config('app.timezone');
|
||||||
$response['app_force_https'] = config('app.force_https');
|
$response['app_force_https'] = config('app.force_https');
|
||||||
$response['session_secure'] = config('session.secure');
|
|
||||||
$response['trusted_proxies'] = config('trustedproxy.proxies');
|
$response['trusted_proxies'] = config('trustedproxy.proxies');
|
||||||
$headers = $request->headers->all();
|
$headers = $request->headers->all();
|
||||||
if (isset($headers['cookie'])) {
|
if (isset($headers['cookie'])) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Kernel extends HttpKernel
|
|||||||
* @var array<int, class-string|string>
|
* @var array<int, class-string|string>
|
||||||
*/
|
*/
|
||||||
protected $middleware = [
|
protected $middleware = [
|
||||||
\App\Http\Middleware\ForceHttps::class,
|
// \App\Http\Middleware\TrustHosts::class,
|
||||||
\App\Http\Middleware\TrustProxies::class,
|
\App\Http\Middleware\TrustProxies::class,
|
||||||
\Illuminate\Http\Middleware\HandleCors::class,
|
\Illuminate\Http\Middleware\HandleCors::class,
|
||||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\URL;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
class ForceHttps
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle an incoming request.
|
|
||||||
*
|
|
||||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
|
||||||
{
|
|
||||||
if (config('app.force_https', false)) {
|
|
||||||
URL::forceScheme('https');
|
|
||||||
$request->server->set('HTTPS', 'on');
|
|
||||||
$request->headers->set('X-Forwarded-Proto', 'https');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
app/Http/Middleware/TrustHosts.php
Normal file
22
app/Http/Middleware/TrustHosts.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||||
|
|
||||||
|
class TrustHosts extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the host patterns that should be trusted.
|
||||||
|
*
|
||||||
|
* @return array<int, string|null>
|
||||||
|
*/
|
||||||
|
public function hosts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$this->allSubdomainsOfApplicationUrl(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -89,6 +90,12 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (config('app.force_https', false)) {
|
||||||
|
URL::forceScheme('https');
|
||||||
|
request()->server->set('HTTPS', 'on');
|
||||||
|
request()->headers->set('X-Forwarded-Proto', 'https');
|
||||||
|
}
|
||||||
|
|
||||||
$this->app->scoped(PermissionStore::class, function (Application $app): PermissionStore {
|
$this->app->scoped(PermissionStore::class, function (Application $app): PermissionStore {
|
||||||
return new PermissionStore;
|
return new PermissionStore;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'secure' => env('SESSION_SECURE_COOKIE', env('APP_FORCE_HTTPS')),
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -29,8 +29,6 @@
|
|||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="APP_FORCE_HTTPS" value="false"/>
|
|
||||||
<env name="TRUSTED_PROXIES" value="0.0.0.0/0,2000:0:0:0:0:0:0:0/3"/>
|
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_DRIVER" value="array"/>
|
<env name="CACHE_DRIVER" value="array"/>
|
||||||
<env name="DB_CONNECTION" value="pgsql_test"/>
|
<env name="DB_CONNECTION" value="pgsql_test"/>
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ function onSelectChange(event: Event) {
|
|||||||
:project="timeEntry.project_id"
|
:project="timeEntry.project_id"
|
||||||
:enable-estimated-time
|
:enable-estimated-time
|
||||||
:currency="currency"
|
:currency="currency"
|
||||||
|
class="border border-border-primary"
|
||||||
:task="
|
:task="
|
||||||
timeEntry.task_id
|
timeEntry.task_id
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ function onSelectChange(event: Event) {
|
|||||||
:show-badge-border="false"
|
:show-badge-border="false"
|
||||||
:project="timeEntry.project_id"
|
:project="timeEntry.project_id"
|
||||||
:currency="currency"
|
:currency="currency"
|
||||||
|
class="border border-border-primary"
|
||||||
:enable-estimated-time
|
:enable-estimated-time
|
||||||
:task="
|
:task="
|
||||||
timeEntry.task_id
|
timeEntry.task_id
|
||||||
|
|||||||
@@ -103,14 +103,10 @@ function setBillableDefaultForProject() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockRefocus = ref(false);
|
|
||||||
|
|
||||||
function onToggleButtonPress(newState: boolean) {
|
function onToggleButtonPress(newState: boolean) {
|
||||||
if (newState) {
|
if (newState) {
|
||||||
emit('startTimer');
|
emit('startTimer');
|
||||||
if (!blockRefocus.value){
|
currentTimeEntryDescriptionInput.value?.focus();
|
||||||
currentTimeEntryDescriptionInput.value?.focus();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
emit('stopTimer');
|
emit('stopTimer');
|
||||||
}
|
}
|
||||||
@@ -133,27 +129,11 @@ function updateTimeEntryDescription() {
|
|||||||
|
|
||||||
const {timeEntries} = storeToRefs(useTimeEntriesStore());
|
const {timeEntries} = storeToRefs(useTimeEntriesStore());
|
||||||
const filteredRecentlyTrackedTimeEntries = computed(() => {
|
const filteredRecentlyTrackedTimeEntries = computed(() => {
|
||||||
// do not include running time entries
|
return timeEntries.value.filter((item) => {
|
||||||
const finishedTimeEntries = timeEntries.value.filter((item) => item.end !== null);
|
|
||||||
|
|
||||||
// filter out duplicates based on description, task, project, tags and billable
|
|
||||||
const nonDuplicateTimeEntries = finishedTimeEntries.filter((item, index, self) => {
|
|
||||||
return index === self.findIndex((t) => (
|
|
||||||
t.description === item.description &&
|
|
||||||
t.task_id === item.task_id &&
|
|
||||||
t.project_id === item.project_id &&
|
|
||||||
t.tags.length === item.tags.length &&
|
|
||||||
t.tags.every((tag) => item.tags.includes(tag)) &&
|
|
||||||
t.billable === item.billable
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// filter time entries based on current description
|
|
||||||
return nonDuplicateTimeEntries.filter((item) => {
|
|
||||||
return item.description
|
return item.description
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
?.includes(tempDescription.value?.toLowerCase()?.trim() || '');
|
?.includes(tempDescription.value?.toLowerCase()?.trim() || '');
|
||||||
}).slice(0, 5);
|
}).slice(0, 5);;
|
||||||
});
|
});
|
||||||
|
|
||||||
const showDropdown = ref(false);
|
const showDropdown = ref(false);
|
||||||
@@ -163,14 +143,6 @@ watch(focused, (focused) => {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// make sure the click event on the dropdown does not get interrupted
|
// make sure the click event on the dropdown does not get interrupted
|
||||||
showDropdown.value = focused
|
showDropdown.value = focused
|
||||||
|
|
||||||
// make sure that the input does not get refocused after the dropdown is closed
|
|
||||||
if(!focused){
|
|
||||||
blockRefocus.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
blockRefocus.value = false;
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,13 @@ const task = computed(() => {
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:data-select-id="timeEntry.id"
|
:data-select-id="timeEntry.id"
|
||||||
:class="twMerge('px-2 py-1.5 flex justify-between items-center space-x-2 w-full rounded', props.highlighted && 'bg-card-background-active')">
|
:class="twMerge('px-2 py-1.5 flex justify-between items-center space-x-2 w-full rounded', props.highlighted && 'bg-card-background-active')">
|
||||||
<span v-if="timeEntry.description !== ''" class="text-sm font-medium">
|
<span class="text-sm font-medium">
|
||||||
{{
|
{{
|
||||||
timeEntry.description
|
timeEntry.description !== ''
|
||||||
|
? timeEntry.description
|
||||||
|
: 'No Description'
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-sm text-text-tertiary font-medium">
|
|
||||||
No Description
|
|
||||||
</span>
|
|
||||||
<ProjectBadge
|
<ProjectBadge
|
||||||
ref="projectDropdownTrigger"
|
ref="projectDropdownTrigger"
|
||||||
:color="project?.color"
|
:color="project?.color"
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ class HealthCheckEndpointTest extends EndpointTestAbstract
|
|||||||
'secure',
|
'secure',
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'timezone',
|
'timezone',
|
||||||
'session_secure',
|
|
||||||
'trusted_proxies',
|
'trusted_proxies',
|
||||||
'url',
|
'url',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Unit\Endpoint\Web;
|
namespace Tests\Unit\Endpoint\Web;
|
||||||
|
|
||||||
use App\Http\Controllers\Web\HomeController;
|
use App\Http\Controllers\Web\HomeController;
|
||||||
use App\Http\Middleware\Authenticate;
|
|
||||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\UsesClass;
|
use PHPUnit\Framework\Attributes\UsesClass;
|
||||||
|
|
||||||
#[CoversClass(HomeController::class)]
|
#[CoversClass(HomeController::class)]
|
||||||
#[CoversClass(Authenticate::class)]
|
|
||||||
#[CoversClass(RedirectIfAuthenticated::class)]
|
|
||||||
#[UsesClass(HomeController::class)]
|
#[UsesClass(HomeController::class)]
|
||||||
class HomeEndpointTest extends EndpointTestAbstract
|
class HomeEndpointTest extends EndpointTestAbstract
|
||||||
{
|
{
|
||||||
@@ -40,17 +36,4 @@ class HomeEndpointTest extends EndpointTestAbstract
|
|||||||
// Assert
|
// Assert
|
||||||
$response->assertRedirect('/login');
|
$response->assertRedirect('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_login_redirects_to_dashboard_if_user_is_logged_in(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$user = User::factory()->withPersonalOrganization()->create();
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->get('/login');
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertRedirect('/dashboard');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,20 +47,6 @@ class EnsureEmailIsVerifiedMiddlewareTest extends MiddlewareTestAbstract
|
|||||||
$response->assertRedirect(route('verification.notice'));
|
$response->assertRedirect(route('verification.notice'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_users_with_unverified_email_get_error_if_the_request_is_json(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
$route = $this->createTestRoute();
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->getJson($route);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertForbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_with_verified_email_can_access_route(): void
|
public function test_users_with_verified_email_can_access_route(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Unit\Middleware;
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
return Route::get('/test-route', function () {
|
|
||||||
return [
|
|
||||||
'is_secure' => request()->secure(),
|
|
||||||
];
|
|
||||||
})->middleware(ForceHttps::class)->uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_if_config_app_force_https_is_true_then_the_request_will_be_modified_to_make_the_app_think_it_was_a_https_request(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Config::set('app.force_https', true);
|
|
||||||
$route = $this->createTestRoute();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->get($route);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertSuccessful();
|
|
||||||
$response->assertJson(['is_secure' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_if_config_app_force_https_is_true_then_the_request_will_be_modified_to_make_the_app_think_it_was_a_https_request_even_if_a_load_balancer_says_it_was_a_http_request(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Config::set('app.force_https', true);
|
|
||||||
$route = $this->createTestRoute();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->get($route, ['X-Forwarded-Proto' => 'http']);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertSuccessful();
|
|
||||||
$response->assertJson(['is_secure' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_if_config_app_force_https_is_false_then_the_request_will_not_be_modified_to_make_the_app_think_it_was_a_https_request(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Config::set('app.force_https', false);
|
|
||||||
$route = $this->createTestRoute();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->get($route);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertSuccessful();
|
|
||||||
$response->assertJson(['is_secure' => false]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_if_config_app_force_https_is_false_then_the_request_will_not_be_modified_but_the_request_can_still_be_https(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Config::set('app.force_https', false);
|
|
||||||
$route = $this->createTestRoute();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->get($route, ['X-Forwarded-Proto' => 'https']);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertSuccessful();
|
|
||||||
$response->assertJson(['is_secure' => true]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user