mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-09 08:42:15 +01:00
Compare commits
3 Commits
feature/fi
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d924fa74ec | ||
|
|
adf0d35c11 | ||
|
|
4ed8f16ae3 |
1
.env.ci
1
.env.ci
@@ -5,7 +5,6 @@ APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
APP_FORCE_HTTPS=false
|
||||
APP_ENABLE_REGISTRATION=true
|
||||
SESSION_SECURE_COOKIE=false
|
||||
|
||||
# Logging
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
@@ -5,7 +5,6 @@ VITE_APP_NAME=solidtime
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_FORCE_HTTPS=true
|
||||
SESSION_SECURE_COOKIE=true
|
||||
OCTANE_SERVER=frankenphp
|
||||
PAGINATION_PER_PAGE_DEFAULT=500
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class UserResource extends Resource
|
||||
{
|
||||
/** @var User|null $record */
|
||||
$record = $form->getRecord();
|
||||
|
||||
return $form
|
||||
->columns(1)
|
||||
->schema([
|
||||
|
||||
@@ -64,6 +64,7 @@ class HealthCheckController extends Controller
|
||||
$response['app_env'] = app()->environment();
|
||||
$response['app_timezone'] = config('app.timezone');
|
||||
$response['app_force_https'] = config('app.force_https');
|
||||
$response['session_secure'] = config('session.secure');
|
||||
$response['trusted_proxies'] = config('trustedproxy.proxies');
|
||||
$headers = $request->headers->all();
|
||||
if (isset($headers['cookie'])) {
|
||||
|
||||
@@ -18,7 +18,7 @@ class Kernel extends HttpKernel
|
||||
* @var array<int, class-string|string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\ForceHttps::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
|
||||
29
app/Http/Middleware/ForceHttps.php
Normal file
29
app/Http/Middleware/ForceHttps.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?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,7 +29,6 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -90,12 +89,6 @@ 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 {
|
||||
return new PermissionStore;
|
||||
});
|
||||
|
||||
@@ -168,7 +168,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
'secure' => env('SESSION_SECURE_COOKIE', env('APP_FORCE_HTTPS')),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
</source>
|
||||
<php>
|
||||
<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="CACHE_DRIVER" value="array"/>
|
||||
<env name="DB_CONNECTION" value="pgsql_test"/>
|
||||
|
||||
@@ -129,7 +129,6 @@ function onSelectChange(event: Event) {
|
||||
:project="timeEntry.project_id"
|
||||
:enable-estimated-time
|
||||
:currency="currency"
|
||||
class="border border-border-primary"
|
||||
:task="
|
||||
timeEntry.task_id
|
||||
"
|
||||
|
||||
@@ -119,7 +119,6 @@ function onSelectChange(event: Event) {
|
||||
:show-badge-border="false"
|
||||
:project="timeEntry.project_id"
|
||||
:currency="currency"
|
||||
class="border border-border-primary"
|
||||
:enable-estimated-time
|
||||
:task="
|
||||
timeEntry.task_id
|
||||
|
||||
@@ -103,10 +103,14 @@ function setBillableDefaultForProject() {
|
||||
}
|
||||
}
|
||||
|
||||
const blockRefocus = ref(false);
|
||||
|
||||
function onToggleButtonPress(newState: boolean) {
|
||||
if (newState) {
|
||||
emit('startTimer');
|
||||
currentTimeEntryDescriptionInput.value?.focus();
|
||||
if (!blockRefocus.value){
|
||||
currentTimeEntryDescriptionInput.value?.focus();
|
||||
}
|
||||
} else {
|
||||
emit('stopTimer');
|
||||
}
|
||||
@@ -129,11 +133,27 @@ function updateTimeEntryDescription() {
|
||||
|
||||
const {timeEntries} = storeToRefs(useTimeEntriesStore());
|
||||
const filteredRecentlyTrackedTimeEntries = computed(() => {
|
||||
return timeEntries.value.filter((item) => {
|
||||
// do not include running time entries
|
||||
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
|
||||
?.toLowerCase()
|
||||
?.includes(tempDescription.value?.toLowerCase()?.trim() || '');
|
||||
}).slice(0, 5);;
|
||||
}).slice(0, 5);
|
||||
});
|
||||
|
||||
const showDropdown = ref(false);
|
||||
@@ -143,6 +163,14 @@ watch(focused, (focused) => {
|
||||
nextTick(() => {
|
||||
// make sure the click event on the dropdown does not get interrupted
|
||||
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,13 +30,14 @@ const task = computed(() => {
|
||||
tabindex="-1"
|
||||
: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')">
|
||||
<span class="text-sm font-medium">
|
||||
<span v-if="timeEntry.description !== ''" class="text-sm font-medium">
|
||||
{{
|
||||
timeEntry.description !== ''
|
||||
? timeEntry.description
|
||||
: 'No Description'
|
||||
timeEntry.description
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="text-sm text-text-tertiary font-medium">
|
||||
No Description
|
||||
</span>
|
||||
<ProjectBadge
|
||||
ref="projectDropdownTrigger"
|
||||
:color="project?.color"
|
||||
|
||||
@@ -82,6 +82,7 @@ class HealthCheckEndpointTest extends EndpointTestAbstract
|
||||
'secure',
|
||||
'timestamp',
|
||||
'timezone',
|
||||
'session_secure',
|
||||
'trusted_proxies',
|
||||
'url',
|
||||
]);
|
||||
|
||||
@@ -5,11 +5,15 @@ declare(strict_types=1);
|
||||
namespace Tests\Unit\Endpoint\Web;
|
||||
|
||||
use App\Http\Controllers\Web\HomeController;
|
||||
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
|
||||
{
|
||||
@@ -36,4 +40,17 @@ class HomeEndpointTest extends EndpointTestAbstract
|
||||
// Assert
|
||||
$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,6 +47,20 @@ class EnsureEmailIsVerifiedMiddlewareTest extends MiddlewareTestAbstract
|
||||
$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
|
||||
{
|
||||
// Arrange
|
||||
|
||||
81
tests/Unit/Middleware/ForceHttpsMiddlewareTest.php
Normal file
81
tests/Unit/Middleware/ForceHttpsMiddlewareTest.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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