add autodeploy and add basic auth for file uploader
Some checks failed
Deploy back2 / deploy (push) Failing after 46s

This commit is contained in:
2026-04-02 19:42:04 +03:00
parent ad4a3367c3
commit b36cf07635
10 changed files with 244 additions and 56 deletions

View File

@@ -68,3 +68,7 @@ FILE_LINKS_ENABLED=false
FILE_LINKS_DISK=local FILE_LINKS_DISK=local
FILE_LINKS_DIRECTORY=temporary-links FILE_LINKS_DIRECTORY=temporary-links
FILE_LINKS_MAX_SIZE_KB=10240 FILE_LINKS_MAX_SIZE_KB=10240
FILE_LINKS_UPLOAD_USER_1=uploader1
FILE_LINKS_UPLOAD_PASSWORD_1=change-me-1
FILE_LINKS_UPLOAD_USER_2=uploader2
FILE_LINKS_UPLOAD_PASSWORD_2=change-me-2

View File

@@ -0,0 +1,68 @@
name: Deploy back2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup SSH key
run: |
set -euo pipefail
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p "${DEPLOY_PORT:-22}" -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
- name: Deploy Laravel
run: |
set -euo pipefail
ssh -p "${DEPLOY_PORT:-22}" "$DEPLOY_USER@$DEPLOY_HOST" "DEPLOY_PATH='$DEPLOY_PATH' bash -s" <<'EOF'
set -euo pipefail
if [ -z "${DEPLOY_PATH:-}" ]; then
echo "DEPLOY_PATH is not set"
exit 1
fi
cd "$DEPLOY_PATH"
if [ ! -d .git ]; then
echo "Expected a git repository at $DEPLOY_PATH"
exit 1
fi
git fetch origin main
git checkout main
git reset --hard origin/main
if [ -f composer.json ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
fi
if [ -f artisan ]; then
php artisan down --retry=60 || true
trap 'php artisan up || true' EXIT
php artisan optimize:clear
php artisan migrate --force
php artisan storage:link || true
php artisan config:cache
php artisan view:cache
php artisan queue:restart || true
fi
EOF

View File

@@ -1,59 +1,50 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center"> ## Repo for site https://back2.iro.kosipov.ru
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel ### Autodeploy (Gitea Actions)
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: Workflow file: `.gitea/workflows/deploy.yml`
- [Simple, fast routing engine](https://laravel.com/docs/routing). Deploy starts automatically on every push to `main`.
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications. Required repository secrets in Gitea:
## Learning Laravel - `DEPLOY_HOST` - host of the production server (example: `back2.iro.kosipov.ru`)
- `DEPLOY_USER` - SSH user for deploy
- `DEPLOY_PATH` - absolute path to Laravel project on server
- `DEPLOY_SSH_KEY` - private SSH key content (for `DEPLOY_USER`)
- `DEPLOY_PORT` - optional, defaults to `22`
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. What deployment does:
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. - updates project to `origin/main`
- runs `composer install --no-dev --optimize-autoloader`
- enables maintenance mode (`php artisan down`)
- clears stale caches, runs migrations, recreates Laravel caches
- restarts queue workers (`php artisan queue:restart`)
- disables maintenance mode (`php artisan up`)
## Laravel Sponsors Server prerequisites:
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). - `php`, `composer`, `git`, `bash` available in PATH
- deploy user has permissions for project files and artisan commands
### Premium Partners ### File Upload Basic Auth
- **[Vehikl](https://vehikl.com)** Upload endpoints and upload page are protected by HTTP Basic Auth with 2 users from `.env`:
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing - `FILE_LINKS_UPLOAD_USER_1`
- `FILE_LINKS_UPLOAD_PASSWORD_1`
- `FILE_LINKS_UPLOAD_USER_2`
- `FILE_LINKS_UPLOAD_PASSWORD_2`
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). Protected routes:
## Code of Conduct - `GET /files`
- `POST /files`
- `POST /api/files/upload`
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). Download routes stay accessible by signed link:
## Security Vulnerabilities - `GET /files/download/{path}` (signed URL)
- `GET /api/files/download/{path}` (signed URL)
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureFileUploadBasicAuth
{
public function handle(Request $request, Closure $next): Response
{
$authorizedUsers = config('file_links.upload_basic_auth_users', []);
$username = (string) $request->getUser();
$password = (string) $request->getPassword();
foreach ($authorizedUsers as $authorizedUser) {
$validUsername = isset($authorizedUser['username']) && hash_equals((string) $authorizedUser['username'], $username);
$validPassword = isset($authorizedUser['password']) && hash_equals((string) $authorizedUser['password'], $password);
if ($validUsername && $validPassword) {
return $next($request);
}
}
return $this->unauthorizedResponse($request);
}
private function unauthorizedResponse(Request $request): Response
{
if ($request->expectsJson()) {
return new JsonResponse(['message' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED, [
'WWW-Authenticate' => 'Basic realm="File Upload"',
]);
}
return response('Unauthorized', Response::HTTP_UNAUTHORIZED, [
'WWW-Authenticate' => 'Basic realm="File Upload"',
]);
}
}

View File

@@ -12,7 +12,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
// $middleware->alias([
'file.upload.basic' => \App\Http\Middleware\EnsureFileUploadBasicAuth::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //

View File

@@ -5,5 +5,14 @@ return [
'disk' => env('FILE_LINKS_DISK', 'local'), 'disk' => env('FILE_LINKS_DISK', 'local'),
'directory' => env('FILE_LINKS_DIRECTORY', 'temporary-links'), 'directory' => env('FILE_LINKS_DIRECTORY', 'temporary-links'),
'max_size_kb' => (int) env('FILE_LINKS_MAX_SIZE_KB', 10240), 'max_size_kb' => (int) env('FILE_LINKS_MAX_SIZE_KB', 10240),
'upload_basic_auth_users' => array_values(array_filter([
[
'username' => env('FILE_LINKS_UPLOAD_USER_1'),
'password' => env('FILE_LINKS_UPLOAD_PASSWORD_1'),
],
[
'username' => env('FILE_LINKS_UPLOAD_USER_2'),
'password' => env('FILE_LINKS_UPLOAD_PASSWORD_2'),
],
], static fn (array $user): bool => filled($user['username']) && filled($user['password']))),
]; ];

View File

@@ -20,7 +20,7 @@ Route::prefix('habits')->group(function () {
}); });
Route::prefix('files')->group(function () { Route::prefix('files')->group(function () {
Route::post('/upload', [FileLinkController::class, 'upload']); Route::post('/upload', [FileLinkController::class, 'upload'])->middleware('file.upload.basic');
Route::get('/download/{path}', [FileLinkController::class, 'download']) Route::get('/download/{path}', [FileLinkController::class, 'download'])
->where('path', '.*') ->where('path', '.*')
->middleware('signed') ->middleware('signed')

View File

@@ -8,8 +8,10 @@ Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
Route::middleware('file.upload.basic')->group(function () {
Route::get('/files', [FileLinkController::class, 'index'])->name('files.index'); Route::get('/files', [FileLinkController::class, 'index'])->name('files.index');
Route::post('/files', [FileLinkController::class, 'uploadWeb'])->name('files.upload'); Route::post('/files', [FileLinkController::class, 'uploadWeb'])->name('files.upload');
});
Route::get('/files/download/{path}', [FileLinkController::class, 'download']) Route::get('/files/download/{path}', [FileLinkController::class, 'download'])
->where('path', '.*') ->where('path', '.*')
->middleware('signed') ->middleware('signed')

View File

@@ -9,14 +9,60 @@ use Tests\TestCase;
class FileLinkControllerTest extends TestCase class FileLinkControllerTest extends TestCase
{ {
public function test_upload_is_unavailable_when_feature_is_disabled(): void protected function setUp(): void
{ {
Config::set('file_links.enabled', false); parent::setUp();
Config::set('file_links.upload_basic_auth_users', [
['username' => 'uploader1', 'password' => 'password1'],
['username' => 'uploader2', 'password' => 'password2'],
]);
}
private function authHeader(string $username = 'uploader1', string $password = 'password1'): array
{
return [
'Authorization' => 'Basic ' . base64_encode($username . ':' . $password),
];
}
public function test_upload_requires_basic_auth(): void
{
Config::set('file_links.enabled', true);
$response = $this->postJson('/api/files/upload', [ $response = $this->postJson('/api/files/upload', [
'file' => UploadedFile::fake()->create('sample.txt', 1), 'file' => UploadedFile::fake()->create('sample.txt', 1),
]); ]);
$response->assertUnauthorized();
$response->assertHeader('WWW-Authenticate', 'Basic realm="File Upload"');
}
public function test_upload_accepts_second_basic_auth_user(): void
{
Storage::fake('local');
Config::set('file_links.enabled', true);
Config::set('file_links.disk', 'local');
Config::set('file_links.directory', 'temporary-links');
$response = $this->withHeaders($this->authHeader('uploader2', 'password2'))->postJson(
'/api/files/upload',
['file' => UploadedFile::fake()->create('sample.txt', 1)]
);
$response->assertOk();
$response->assertJsonStructure(['path', 'expiresAt', 'url']);
}
public function test_upload_is_unavailable_when_feature_is_disabled(): void
{
Config::set('file_links.enabled', false);
$response = $this->withHeaders($this->authHeader())->postJson(
'/api/files/upload',
['file' => UploadedFile::fake()->create('sample.txt', 1)]
);
$response->assertNotFound(); $response->assertNotFound();
} }
@@ -27,9 +73,10 @@ class FileLinkControllerTest extends TestCase
Config::set('file_links.disk', 'local'); Config::set('file_links.disk', 'local');
Config::set('file_links.directory', 'temporary-links'); Config::set('file_links.directory', 'temporary-links');
$response = $this->postJson('/api/files/upload', [ $response = $this->withHeaders($this->authHeader())->postJson(
'file' => UploadedFile::fake()->create('sample.txt', 1), '/api/files/upload',
]); ['file' => UploadedFile::fake()->create('sample.txt', 1)]
);
$response->assertOk(); $response->assertOk();
$response->assertJsonStructure(['path', 'expiresAt', 'url']); $response->assertJsonStructure(['path', 'expiresAt', 'url']);
@@ -50,4 +97,3 @@ class FileLinkControllerTest extends TestCase
$downloadResponse->assertHeader('content-disposition'); $downloadResponse->assertHeader('content-disposition');
} }
} }

View File

@@ -9,11 +9,36 @@ use Tests\TestCase;
class FileLinkWebTest extends TestCase class FileLinkWebTest extends TestCase
{ {
protected function setUp(): void
{
parent::setUp();
Config::set('file_links.upload_basic_auth_users', [
['username' => 'uploader1', 'password' => 'password1'],
['username' => 'uploader2', 'password' => 'password2'],
]);
}
private function authHeader(string $username = 'uploader1', string $password = 'password1'): array
{
return [
'Authorization' => 'Basic ' . base64_encode($username . ':' . $password),
];
}
public function test_files_page_requires_basic_auth(): void
{
$response = $this->get('/files');
$response->assertUnauthorized();
$response->assertHeader('WWW-Authenticate', 'Basic realm="File Upload"');
}
public function test_files_page_shows_disabled_state(): void public function test_files_page_shows_disabled_state(): void
{ {
Config::set('file_links.enabled', false); Config::set('file_links.enabled', false);
$response = $this->get('/files'); $response = $this->withHeaders($this->authHeader())->get('/files');
$response->assertOk(); $response->assertOk();
$response->assertSee('Функция отключена конфигом'); $response->assertSee('Функция отключена конфигом');
@@ -26,7 +51,7 @@ class FileLinkWebTest extends TestCase
Config::set('file_links.disk', 'local'); Config::set('file_links.disk', 'local');
Config::set('file_links.directory', 'temporary-links'); Config::set('file_links.directory', 'temporary-links');
$response = $this->post('/files', [ $response = $this->withHeaders($this->authHeader())->post('/files', [
'file' => UploadedFile::fake()->create('web-sample.txt', 1), 'file' => UploadedFile::fake()->create('web-sample.txt', 1),
]); ]);
@@ -35,4 +60,3 @@ class FileLinkWebTest extends TestCase
$response->assertSessionHas('file_expires_at'); $response->assertSessionHas('file_expires_at');
} }
} }