Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ tasks:
- task: _set-app-key
- cmd: "{{.DOCKER_EXEC_USER}} npm install"
- cmd: "{{.DOCKER_EXEC_USER}} php artisan migrate"
- task: create-test-db

frp:init:
desc: "Add default FRP domains (api, app, mail)"
Expand Down Expand Up @@ -110,9 +111,9 @@ tasks:
- cmd: "{{.DOCKER_EXEC_USER}} php artisan {{.CLI_ARGS}}"

test:
desc: "Run pest tests"
desc: "Run tests"
cmds:
- cmd: "{{.DOCKER_EXEC_USER}} vendor/bin/pest {{.CLI_ARGS}}"
- cmd: "{{.DOCKER_EXEC_USER}} composer test"

fix:
desc: "Run fixers"
Expand All @@ -135,3 +136,16 @@ tasks:
echo "APP_KEY is not set. Creating:"
docker compose exec {{.DOCKER_COMPOSE_APP_CONTAINER}} php artisan key:generate
fi

create-test-db:
desc: "Create Postgres test database inside db container (if not exists)"
vars:
DOCKER_COMPOSE_DATABASE_CONTAINER: database
DATABASE_USERNAME: '{{default .DB_USERNAME "MiniStravaAPI"}}'
TEST_DATABASE_NAME: '{{default .TEST_DB_DATABASE "MiniStravaAPI-test"}}'
cmds:
- |
docker compose exec {{.DOCKER_COMPOSE_DATABASE_CONTAINER}} \
bash -lc 'createdb --username={{.DATABASE_USERNAME}} {{.TEST_DATABASE_NAME}} &> /dev/null \
&& echo "Created database for tests ({{.TEST_DATABASE_NAME}})." \
|| echo "Database for tests ({{.TEST_DATABASE_NAME}}) exists."'
17 changes: 17 additions & 0 deletions app/Actions/Avatars/ChangeAvatarAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Strava\Actions\Avatars;

use Illuminate\Http\UploadedFile;

class ChangeAvatarAction
{
public function execute(UploadedFile $uploadedFile, int $userId): bool
{
$uploadedFile->storeAs("", $userId . ".png", "avatars");

return true;
}
}
23 changes: 23 additions & 0 deletions app/Actions/Avatars/DeleteAvatarAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Strava\Actions\Avatars;

use Illuminate\Support\Facades\Storage;

class DeleteAvatarAction
{
public function execute(int $userId): bool
{
$filename = $userId . ".png";

if (Storage::disk("avatars")->exists($filename)) {
Storage::disk("avatars")->delete($filename);

return true;
}

return false;
}
}
21 changes: 21 additions & 0 deletions app/Actions/Avatars/GetAvatarAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Strava\Actions\Avatars;

use Illuminate\Support\Facades\Storage;

class GetAvatarAction
{
public function execute(int $userId): ?string
{
$filename = $userId . ".png";

if (Storage::disk("avatars")->exists($filename)) {
return Storage::disk("avatars")->get($filename);
}

return null;
}
}
18 changes: 18 additions & 0 deletions app/Actions/Avatars/GetDefaultAvatarAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Strava\Actions\Avatars;

use Identicon\Generator\SvgGenerator;
use Identicon\Identicon;

class GetDefaultAvatarAction
{
public function execute(int $userId): ?string
{
$identicon = new Identicon(new SvgGenerator());

return $identicon->getImageData((string)$userId, 300);
}
}
18 changes: 18 additions & 0 deletions app/Actions/Profile/UpdateProfileAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Strava\Actions\Profile;

use Strava\Models\User;

class UpdateProfileAction
{
public function execute(User $user, array $data): ?User
{
$user->fill($data);
$user->save();

return $user->fresh();
}
}
11 changes: 11 additions & 0 deletions app/Enums/Gender.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Strava\Enums;

enum Gender: string
{
case Male = "male";
case Female = "female";
}
39 changes: 39 additions & 0 deletions app/Helpers/IdenticonHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Strava\Helpers;

use Identicon\Identicon;
use Illuminate\Support\Facades\Storage;

class IdenticonHelper
{
private Identicon $identicon;

public function __construct()
{
$this->identicon = new Identicon();
}

public static function url(string|int $name): string
{
return url("/api/profiles/{$name}/avatar");
}

public function create(string|int $filename, string $data): string
{
$image = $this->identicon->getImageData($data, 300);

return $this->save($filename, $image);
}

private function save(string|int $name, string $imageData): string
{
$path = $name . ".png";

Storage::disk("avatars")->put($path, $imageData);

return Storage::disk("avatars")->url($path);
}
}
75 changes: 75 additions & 0 deletions app/Http/Controllers/Profile/ProfileController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Strava\Http\Controllers\Profile;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Strava\Actions\Avatars\ChangeAvatarAction;
use Strava\Actions\Avatars\DeleteAvatarAction;
use Strava\Actions\Avatars\GetAvatarAction;
use Strava\Actions\Avatars\GetDefaultAvatarAction;
use Strava\Actions\Profile\UpdateProfileAction;
use Strava\Http\Controllers\Controller;
use Strava\Http\Requests\ChangeAvatarRequest;
use Strava\Http\Requests\UpdateProfileRequest;
use Strava\Http\Resources\UserResource;

class ProfileController extends Controller
{
public function update(UpdateProfileRequest $request, UpdateProfileAction $updateProfileAction): UserResource
{
$validated = $request->validated();

$user = $request->user();
$updated = $updateProfileAction->execute($user, $validated);

return UserResource::make($updated);
}

public function show(Request $request): UserResource
{
$user = $request->user();

return UserResource::make($user);
}

public function changeAvatar(ChangeAvatarRequest $request, ChangeAvatarAction $changeAvatarAction): UserResource
{
$user = $request->user();

$changeAvatarAction->execute(
$request->file("avatar"),
$user->id,
);

return UserResource::make($user);
}

public function getAvatar(int $userId, GetAvatarAction $getAvatarAction, GetDefaultAvatarAction $getDefaultAvatarAction): Response
{
$avatar = $getAvatarAction->execute($userId);

if ($avatar) {
return response($avatar)
->header("Content-Type", "image/png")
->header("Cache-Control", "public, max-age=31536000");
}

$defaultAvatar = $getDefaultAvatarAction->execute($userId);

return response($defaultAvatar)
->header("Content-Type", "image/svg+xml")
->header("Cache-Control", "public, max-age=86400");
}

public function deleteAvatar(Request $request, DeleteAvatarAction $deleteProfileAction): UserResource
{
$user = $request->user();

$deleteProfileAction->execute($user->id);

return UserResource::make($user);
}
}
42 changes: 42 additions & 0 deletions app/Http/Controllers/Profile/ProfilesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Strava\Http\Controllers\Profile;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Strava\Http\Controllers\Controller;
use Strava\Http\Resources\UserResource;
use Strava\Models\User;
use Symfony\Component\HttpFoundation\Response;

class ProfilesController extends Controller
{
public function index(): AnonymousResourceCollection
{
$users = User::query()->select([
"id",
"name",
"first_name",
"last_name",
"birth_date",
"height",
"weight",
"gender",
])->paginate(10);

return UserResource::collection($users);
}

public function show(int $id): JsonResponse
{
$user = User::query()->find($id);

if ($user === null) {
return response()->json([], Response::HTTP_NOT_FOUND);
}

return response()->json(UserResource::make($user), Response::HTTP_OK);
}
}
31 changes: 31 additions & 0 deletions app/Http/Requests/ChangeAvatarRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Strava\Http\Requests;

use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;

class ChangeAvatarRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return auth()->check();
}

/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
"avatar" => ["required", "image", "mimes:png", "max:2048"], // max 2MB
];
}
}
29 changes: 29 additions & 0 deletions app/Http/Requests/UpdateProfileRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Strava\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
use Strava\Enums\Gender;

class UpdateProfileRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->check();
}

public function rules(): array
{
return [
"first_name" => ["nullable", "string", "max:255"],
"last_name" => ["nullable", "string", "max:255"],
"birth_date" => ["nullable", "date", "before:today"],
"height" => ["nullable", "integer", "min:50", "max:300"],
"weight" => ["nullable", "numeric", "min:10", "max:300"],
"gender" => ["nullable", new Enum(Gender::class)],
];
}
}
Loading
Loading