diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 9d2b9a4..8de2764 100755 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -25,15 +25,15 @@ jobs: - name: Build app run: - sudo docker-compose up -d --build && + sudo docker-compose -f dev-docker-compose.yml up -d --build && sudo chmod -R 777 $(pwd)/www && - sudo docker-compose run php composer update && - sudo docker-compose run php composer upgrade && - sudo docker-compose run php composer install && - sudo docker-compose run php php artisan migrate + sudo docker-compose -f dev-docker-compose.yml run php composer update && + sudo docker-compose -f dev-docker-compose.yml run php composer upgrade && + sudo docker-compose -f dev-docker-compose.yml run php composer install && + sudo docker-compose -f dev-docker-compose.yml run php php artisan migrate - name: Make seed - run: sudo docker-compose run php php artisan db:seed + run: sudo docker-compose -f dev-docker-compose.yml run php php artisan db:seed # - name: Testing app # run: sudo docker-compose run php php artisan test diff --git a/dev-docker-compose.yml b/dev-docker-compose.yml new file mode 100755 index 0000000..95ba38a --- /dev/null +++ b/dev-docker-compose.yml @@ -0,0 +1,83 @@ +version: "3" + +services: + web: + build: + context: . + dockerfile: ./etc/nginx/nginx.Dockerfile + container_name: beautiful-web + ports: + - '80:80' + volumes: + - ./www:/var/www + depends_on: + - php + networks: + - beautiful-net + php: + build: + context: . + dockerfile: ./etc/php/php.dev.Dockerfile + ports: + - '9000:9000' + depends_on: + - cache + - db + volumes: + - ./www:/var/www + networks: + - beautiful-net + + cache: + build: + context: . + dockerfile: ./etc/redis/redis.Dockerfile + ports: + - '6379:6379' + networks: + - beautiful-net + volumes: + - volume-cache:/data + + db: + build: + context: . + dockerfile: ./etc/postgresql/postgres.Dockerfile + env_file: + - ./etc/postgresql/.env + ports: + - '5432:5432' + volumes: + - volume-db:/var/lib/postgresql/data + networks: + - beautiful-net + + s3: + image: adobe/s3mock + restart: unless-stopped + environment: + - debug=true + - COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS=laravel + - COM_ADOBE_TESTING_S3MOCK_STORE_RETAIN_FILES_ON_EXIT=true + - COM_ADOBE_TESTING_S3MOCK_STORE_ROOT=containers3root + ports: + - '9090:9090' + volumes: + - ./locals3root:/containers3root + networks: + - beautiful-net + + adminer: + image: adminer + ports: + - '8080:8080' + networks: + - beautiful-net + +networks: + beautiful-net: + driver: bridge + +volumes: + volume-db: + volume-cache: diff --git a/etc/php/php.dev.Dockerfile b/etc/php/php.dev.Dockerfile new file mode 100755 index 0000000..9df3e4c --- /dev/null +++ b/etc/php/php.dev.Dockerfile @@ -0,0 +1,38 @@ +FROM php:8.4-fpm-alpine + +WORKDIR /var/www + +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +RUN apk update && apk --no-cache add \ + build-base \ + libpng-dev \ + libjpeg-turbo-dev \ + libwebp-dev \ + libxpm-dev \ + freetype-dev \ + libzip-dev \ + zip \ + unzip \ + git \ + bash \ + fcgi \ + libmcrypt-dev \ + oniguruma-dev \ + postgresql-dev + +RUN rm -rf /var/cache/apk/* + +RUN docker-php-ext-configure gd \ + --with-freetype=/usr/include/ \ + --with-jpeg=/usr/include/ \ + --with-webp=/usr/include/ \ + && docker-php-ext-install gd \ + && docker-php-ext-install pdo pdo_pgsql mbstring zip exif pcntl bcmath opcache + +RUN chown 1000:1000 -R /var/www +RUN find . -type d -exec chmod 775 {} \; && \ + find . -type f -exec chmod 664 {} \; + +USER 1000 +CMD ["php-fpm"] \ No newline at end of file diff --git a/www/.env.example b/www/.env.example index db5dd27..221b3aa 100644 --- a/www/.env.example +++ b/www/.env.example @@ -5,6 +5,8 @@ APP_DEBUG=true APP_TIMEZONE=UTC APP_URL=http://localhost +ADMIN_PASSWORD=password + APP_LOCALE=en APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US diff --git a/www/app/Modules/Admin/Admin.php b/www/app/Modules/Admin/Admin.php new file mode 100644 index 0000000..c5bdac5 --- /dev/null +++ b/www/app/Modules/Admin/Admin.php @@ -0,0 +1,39 @@ +name; + } + + public function getVersion(): string + { + return $this->version; + } +} \ No newline at end of file diff --git a/www/app/Modules/Admin/Controllers/AdminController.php b/www/app/Modules/Admin/Controllers/AdminController.php new file mode 100644 index 0000000..6649f45 --- /dev/null +++ b/www/app/Modules/Admin/Controllers/AdminController.php @@ -0,0 +1,23 @@ + Cache::remember('users_count', 60, fn () => UserRepository::getAll()->count()), + 'postsCount' => Cache::remember('posts_count', 60, fn () => PostRepository::getAll()->count()), + 'commentCount' => Cache::remember('comments_count', 60, fn () => CommentRepository::getAll()->count()), + ]); + + } +} diff --git a/www/app/Modules/Admin/Controllers/Contents/CommentsAdminController.php b/www/app/Modules/Admin/Controllers/Contents/CommentsAdminController.php new file mode 100644 index 0000000..09ffcbb --- /dev/null +++ b/www/app/Modules/Admin/Controllers/Contents/CommentsAdminController.php @@ -0,0 +1,51 @@ +getData($request, 'comments', (new CommentRepository)); + + $result = parent::index($request, $data); + if($result) { + return $result; + } + + $comment = new Comment; + $contents = $this->getContents($comment); + + return view('admin.content.index', [ + 'fillables' => $this->getFillables($comment), + 'contents' => $contents, + 'currentPage' => $contents->currentPage(), + 'lastPage' => $contents->lastPage(), + 'route' => 'admin.comments', + ]); + } + + private function getFillables(Comment $comment): array + { + $fillables = $comment->getFillable(); + $fillables = array_merge(['id',], $fillables); + $fillables = array_merge($fillables, ['created_at', 'updated_at']); + + return $fillables; + } + + private function getContents(Comment $comment): LengthAwarePaginator + { + return $comment->limit(20) + ->paginate(); + } +} diff --git a/www/app/Modules/Admin/Controllers/Contents/PostsAdminController.php b/www/app/Modules/Admin/Controllers/Contents/PostsAdminController.php new file mode 100644 index 0000000..7c97718 --- /dev/null +++ b/www/app/Modules/Admin/Controllers/Contents/PostsAdminController.php @@ -0,0 +1,54 @@ +getData($request, 'posts', (new PostRepository)); + + $result = parent::index($request, $data); + if($result) { + return $result; + } + + $post = new Post; + $contents = $this->getContents($post); + + return view('admin.content.index', [ + 'fillables' => $this->getFillables($post), + 'contents' => $contents, + 'currentPage' => $contents->currentPage(), + 'lastPage' => $contents->lastPage(), + 'route' => 'admin.posts' + ]); + } + + private function getFillables(Post $post): array + { + $fillables = $post->getFillable(); + $fillables = array_merge(['id',], $fillables); + $fillables = array_merge($fillables, ['created_at', 'updated_at']); + + return $fillables; + } + + private function getContents(Post $post): LengthAwarePaginator + { + return $post + ->limit(20) + ->paginate(); + } +} diff --git a/www/app/Modules/Admin/Controllers/Contents/RolesAdminController.php b/www/app/Modules/Admin/Controllers/Contents/RolesAdminController.php new file mode 100644 index 0000000..264f906 --- /dev/null +++ b/www/app/Modules/Admin/Controllers/Contents/RolesAdminController.php @@ -0,0 +1,51 @@ +getData($request, 'roles', (new RoleRepository)); + + $result = parent::index($request, $data); + if($result) { + return $result; + } + + $role = new Role; + $contents = $this->getContents($role); + + return view('admin.content.index', [ + 'fillables' => $this->getFillables($role), + 'contents' => $contents, + 'currentPage' => $contents->currentPage(), + 'lastPage' => $contents->lastPage(), + 'route' => 'admin.roles', + ]); + } + + private function getFillables(Role $role): array + { + $fillables = $role->getFillable(); + $fillables = array_merge(['id',], $fillables); + $fillables = array_merge($fillables, ['created_at', 'updated_at']); + + return $fillables; + } + + private function getContents(Role $role): LengthAwarePaginator + { + return $role->limit(20) + ->paginate(); + } +} diff --git a/www/app/Modules/Admin/Controllers/Contents/UsersAdminController.php b/www/app/Modules/Admin/Controllers/Contents/UsersAdminController.php new file mode 100644 index 0000000..cb4d8dd --- /dev/null +++ b/www/app/Modules/Admin/Controllers/Contents/UsersAdminController.php @@ -0,0 +1,52 @@ +getData($request, 'users', (new UserRepository)); + + $result = parent::index($request, $data); + if($result) { + return $result; + } + + $user = new User; + $contents = $this->getContents($user); + + return view('admin.content.index', [ + 'fillables' => $this->getFillables($user), + 'contents' => $contents, + 'currentPage' => $contents->currentPage(), + 'lastPage' => $contents->lastPage(), + 'route' => 'admin.users' + ]); + } + + private function getFillables(User $user): array + { + $fillables = $user->getFillable(); + $fillables = array_merge(['id',], $fillables); + $fillables = array_merge($fillables, ['created_at', 'updated_at']); + + return $fillables; + } + + private function getContents(User $user): LengthAwarePaginator + { + return $user + ->limit(20) + ->paginate(); + } +} diff --git a/www/app/Modules/Admin/Controllers/Controller.php b/www/app/Modules/Admin/Controllers/Controller.php new file mode 100644 index 0000000..e6cde54 --- /dev/null +++ b/www/app/Modules/Admin/Controllers/Controller.php @@ -0,0 +1,260 @@ + 'number', + 'varchar' => 'text', + 'jsonb' => 'object', + 'string' => 'text', + 'text' => 'textarea', + 'integer' => 'number', + 'bigint' => 'number', + 'float' => 'number', + 'double' => 'number', + 'decimal' => 'number', + 'boolean' => 'checkbox', + 'date' => 'date', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'json' => 'object', + ]; + + public function index(Request $request, array $data = []): View|RedirectResponse|null + { + if($request->isMethod('GET')) { + if($request->has('remove') && $request->input('remove') === 'Y') { + return $this->remove($request, $data['repository']); + } + + if( + ($request->has('create') && $request->get('create') === 'Y') || + ($request->has('update') && $request->get('update') === 'Y') + ) { + return $this->show($data); + } + } + + if($request->isMethod('POST')) { + return $this->create($request, $data['validator'], $data['repository']); + } + + if($request->isMethod('PATCH')) { + return $this->update($request, $data['validator'], $data['repository']); + } + + return null; + } + + public function show(array $data): View + { + if(!isset($data['items']) || count($data['items']) === 0) { + throw new \LogicException('Not found item'); + } + + return view('admin.content.detail', data: $data); + } + + public function remove(Request $request, RepositoryInterface $repository): RedirectResponse + { + if(!$request->has('id')) { + return back()->withErrors('Not found item'); + } + + $id = $request->get('id'); + if(filter_var($id, FILTER_VALIDATE_INT) === false) { + return back()->withErrors('Not found item'); + } + + if(!$repository::remove(['id' => (int)$id])) { + return back()->withErrors('Can not remove'); + } + + $url = explode('?', url()->current()); + + $url = $url[0]; + if($request->has('page')) { + $url = $url . '?page=' . $request->get('page'); + } + + return redirect($url) + ->with('success', 'Remove element is complete!'); + } + + public function update( + Request $request, + array $validation, + RepositoryInterface $repository + ): RedirectResponse { + if($request->has('picture') && gettype($request->get('picture')) === 'string' ) { + $file = $this->fileUploadFromUrl($request->get('picture')); + $request->merge(['picture' => $file]); + } + + if($request->has('file') && gettype($request->get('file')) === 'string' ) { + $file = $this->fileUploadFromUrl($request->get('file')); + $request->merge(['file' => $file]); + } + + $data = $request->validate($validation); + + if($request->has('picture')) { + $data['picture'] = $this->saveFile($data['picture']); + } + + if($request->has('file')) { + $data['file'] = $this->saveFile($data['file']); + } + + if($request->has('id')) { + $data['id'] = $request->get('id'); + } + + $isUpdate = false; + try { + $isUpdate = $repository::update($data); + } catch (\Exception $e) { + return back()->withErrors($e->getMessage()); + } + + if($isUpdate === false || $isUpdate === 0) { + return back()->withErrors('Can`t be update element'); + } + + $url = explode('?', url()->current()); + return redirect($url[0]) + ->with('success', 'Update element is complete!'); + } + + public function create( + Request $request, + array $validation, + RepositoryInterface $repository, + ): RedirectResponse { + $data = $request->validate($validation); + + if($request->has('picture')) { + $data['picture'] = $this->saveFile($data['picture']); + } + + if($request->has('file')) { + $data['file'] = $this->saveFile($data['file']); + } + + $isSave = false; + try { + $isSave = $repository::save($data); + } catch (\Exception $e) { + return back()->withErrors($e->getMessage()); + } + + if(!$isSave) { + return back()->withErrors('Can`t be created element'); + } + + $url = explode('?', url()->current()); + return redirect($url[0]) + ->with('success', 'Created element is complete!'); + } + + public function getData(Request $request, string $table, RepositoryInterface $repository): array + { + $model = null; + if($request->has('id') && $request->get('id') > 0) { + $model = $repository::getById($request->get('id')); + } + + $data = [ + 'method' => 'POST', + 'items' => [], + 'validator' => (new TableValidatorService)->generateValidationRulesTable($table), + 'repository' => $repository, + ]; + + $columns = Schema::getColumnListing($table); + foreach($columns as $column) { + $type = Schema::getColumnType($table, $column); + + if($column == 'id' || $column === 'created_at' || $column === 'updated_at') { + continue; + } + + if(str_contains($type, 'int')) { + $type = 'number'; + } else { + $type = $this->types[$type]; + } + + $data['items'][$column]['column'] = $column; + $data['items'][$column]['type'] = $type; + } + + if($model) { + $data['method'] = 'PATCH'; + foreach($model->toArray() as $fillable => $item) { + if(!isset($data['items'][$fillable])) { + continue; + } + + $data['items'][$fillable] = array_merge( + $data['items'][$fillable], + ['value' => $item,] + ); + } + } + + return $data; + } + + private function saveFile($file) + { + if($file) { + if(!S3Storage::putFile('/', $file)) { + return back()->withErrors("Update profile data failed"); + } + + $file = S3Storage::getFile($file->hashName()); + } + + return $file; + } + + private function fileUploadFromUrl(string $imageUrl) + { + try { + $response = Http::get($imageUrl); + + if ($response->successful()) { + // Создаем временный файл + $tempFile = tempnam(sys_get_temp_dir(), 'laravel_upload'); + file_put_contents($tempFile, $response->body()); + + // Получаем информацию о файле + $mimeType = $response->getHeader('Content-Type')[0] ?? 'image/jpeg'; + $originalName = basename(parse_url($imageUrl, PHP_URL_PATH)); + + // Создаем UploadedFile + return new \Illuminate\Http\UploadedFile( + $tempFile, + $originalName, + $mimeType, + null, + true + ); + } + } catch (\Exception $e) { + return $e->getMessage(); + } + } +} diff --git a/www/app/Modules/Admin/Middleware/PremisonalRole.php b/www/app/Modules/Admin/Middleware/PremisonalRole.php new file mode 100644 index 0000000..9434beb --- /dev/null +++ b/www/app/Modules/Admin/Middleware/PremisonalRole.php @@ -0,0 +1,33 @@ +check()) { + return redirect()->route('auth.login'); + } + + $role = auth()->user()->role()->get(); + $permissions = array_column($role->toArray(), 'permissions'); + + if(!in_array(1, $permissions)) { + return redirect()->route('master.home'); + } + + return $next($request); + } +} diff --git a/www/app/Modules/Admin/Routes/Route.php b/www/app/Modules/Admin/Routes/Route.php new file mode 100644 index 0000000..4220582 --- /dev/null +++ b/www/app/Modules/Admin/Routes/Route.php @@ -0,0 +1,42 @@ +middleware(['auth', PremisonalRole::class]) + ->prefix('admin') + ->group(fn() => self::routers()) + ->name('admin.routes'); + } + + private static function routers(): void + { + self::get('/', 'AdminController@index')->name('admin.home'); + + self::match( + ['GET', 'POST', 'PATCH', 'DELETE',], + '/users', 'Contents\UsersAdminController@index') + ->name('admin.users'); + + self::match( + ['GET', 'POST', 'PATCH', 'DELETE',], + '/posts', 'Contents\PostsAdminController@index') + ->name('admin.posts'); + + self::match( + ['GET', 'POST', 'PATCH', 'DELETE',], + '/roles', 'Contents\RolesAdminController@index') + ->name('admin.roles'); + + self::match( + ['GET', 'POST', 'PATCH', 'DELETE',], + '/comments', 'Contents\CommentsAdminController@index') + ->name('admin.comments'); + } +} diff --git a/www/app/Modules/Auth/Controllers/RegistrationController.php b/www/app/Modules/Auth/Controllers/RegistrationController.php index 342ba5a..88c8587 100644 --- a/www/app/Modules/Auth/Controllers/RegistrationController.php +++ b/www/app/Modules/Auth/Controllers/RegistrationController.php @@ -38,6 +38,7 @@ function store(Request $request): RedirectResponse 'username' => $data['username'], 'email' => $data['email'], 'password' => bcrypt($data['password']), + 'role_id' => 2, ]); if(!$isSaved) { return back()->withErrors('Failed to registration user'); diff --git a/www/app/Modules/Master/Lib/TableValidatorService.php b/www/app/Modules/Master/Lib/TableValidatorService.php new file mode 100644 index 0000000..9f49034 --- /dev/null +++ b/www/app/Modules/Master/Lib/TableValidatorService.php @@ -0,0 +1,160 @@ + 'text', + 'text' => 'textarea', + 'integer' => 'number', + 'bigint' => 'number', + 'float' => 'number', + 'double' => 'number', + 'decimal' => 'number', + 'boolean' => 'checkbox', + 'date' => 'date', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'json' => 'object', + ]; + + public function generateValidationRulesTable(string $table, $ignoreId = null): array + { + $rules = []; + $columns = Schema::getColumnListing($table); + + foreach ($columns as $column) { + if ($column === 'id' || $column === 'created_at' || $column === 'updated_at') { + continue; + } + + $rules[$column] = $this->generateValidationRules($table, $column); + + foreach ($rules[$column] as $key => $rule) { + if (is_string($rule) && strpos($rule, 'unique:') === 0 && $ignoreId) { + $rules[$column][$key] = Rule::unique($table, $column)->ignore($ignoreId); + } + } + } + + return $rules; + } + + protected function generateValidationRules(string $table, string $column): array + { + $rules = []; + $type = Schema::getColumnType($table, $column); + + switch ($type) { + case 'integer': + case 'bigint': + $rules[] = 'integer'; + break; + case 'float': + case 'double': + case 'decimal': + $rules[] = 'numeric'; + break; + case 'boolean': + $rules[] = 'boolean'; + break; + case 'date': + $rules[] = 'date'; + break; + case 'datetime': + case 'timestamp': + $rules[] = 'date_format:Y-m-d H:i:s'; + break; + case 'json': + $rules[] = 'json'; + break; + default: + if(in_array($column, ['picture', 'file'])) { + $rules[] = 'image'; + $rules[] = 'max:30004'; + } else { + $rules[] = 'string'; + } + } + + $nullable = $this->isColumnNullable($table, $column); + if (!$nullable) { + array_unshift($rules, 'required'); + } else { + $rules[] = 'nullable'; + } + + if (in_array($type, ['string', 'text'])) { + $maxLength = $this->getColumnMaxLength($table, $column); + if ($maxLength) { + $rules[] = "max:{$maxLength}"; + } + } + + if ($column !== 'id' && $this->isColumnUnique($table, $column)) { + $rules[] = 'unique:' . $table . ',' . $column; + } + + return $rules; + } + + protected function isColumnNullable(string $table, string $column): bool + { + if(!Schema::hasColumn($table, $column)) { + return false; + } + + $columnsSchema = Schema::getColumns($table); + foreach($columnsSchema as $columnSchema) { + if($columnSchema['name'] === $column) { + return $columnSchema['nullable']; + } + } + + return false; + } + + protected function getColumnMaxLength(string $table, string $column): ?int + { + if(!Schema::hasColumn($table, $column)) { + return null; + } + + $columnsSchema = Schema::getColumns($table); + foreach($columnsSchema as $columnSchema) { + if($columnSchema['name'] === $column) { + if($columnSchema['type_name'] == 'varchar') { + $number = str_replace('varying(', $columnSchema['type']); + $number = str_replace(')', '', $number); + + return (int)$number; + } + + return 255; + } + } + + return null; + } + + protected function isColumnUnique(string $table, string $column): bool + { + if(!Schema::hasColumn($table, $column)) { + return false; + } + + $columnsSchema = Schema::getColumns($table); + foreach($columnsSchema as $columnSchema) { + if($columnSchema['name'] === $column) { + return str_contains('unique', $columnSchema['type']) || (isset($columnSchema['unique']) && $columnSchema['unique']); + } + } + + return false; + } +} diff --git a/www/app/Modules/Master/Models/Role.php b/www/app/Modules/Master/Models/Role.php new file mode 100644 index 0000000..84e78fa --- /dev/null +++ b/www/app/Modules/Master/Models/Role.php @@ -0,0 +1,25 @@ +BelongsTo(User::class, 'role_id', 'id'); + } +} diff --git a/www/app/Modules/Master/Models/User.php b/www/app/Modules/Master/Models/User.php index fea09d4..629cb46 100644 --- a/www/app/Modules/Master/Models/User.php +++ b/www/app/Modules/Master/Models/User.php @@ -8,6 +8,7 @@ use App\Modules\Profile\Models\Post; use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -22,14 +23,12 @@ class User extends Authenticatable * @var array */ protected $fillable = [ - 'name', 'username', 'email', 'password', 'remember_token', 'picture', 'description', - 'permissions', 'email_verified_at', 'socialnetworks', ]; @@ -69,6 +68,11 @@ function like(): HasMany return $this->hasMany(Like::class); } + function role(): BelongsTo + { + return $this->belongsTo(Role::class, 'role_id'); + } + /** * Get the attributes that should be cast. * diff --git a/www/app/Modules/Master/Repository/RoleRepository.php b/www/app/Modules/Master/Repository/RoleRepository.php new file mode 100644 index 0000000..6354f2c --- /dev/null +++ b/www/app/Modules/Master/Repository/RoleRepository.php @@ -0,0 +1,83 @@ +name = $data['name']; + $role->slug = $data['slug']; + $role->permissions = $data['permissions']; + + return $role->save(); + } + + /** + * @inheritDoc + */ + public static function remove(array $data): bool + { + if(!isset($data['id'])) { + throw new \InvalidArgumentException('Required (id) parameter is missing'); + } + + if(!is_int($data['id'])) { + throw new \InvalidArgumentException('Required (id) parameter must be integer'); + } + + + return Role::query()->where('id', $data['id'])->delete(); + } + + /** + * @inheritDoc + */ + public static function update(array $data): int + { + // TODO: Implement update() method. + } + + /** + * @inheritDoc + */ + public static function getAll(): Collection + { + return Role::all(); + } + + /** + * @inheritDoc + */ + public static function getById(int $id): Role|false + { + return Role::query()->find($id); + } + + /** + * @inheritDoc + */ + public static function getBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): Collection + { + return Collection::make([]); + } +} diff --git a/www/app/Modules/Master/Repository/UserRepository.php b/www/app/Modules/Master/Repository/UserRepository.php index d45340e..e787e97 100644 --- a/www/app/Modules/Master/Repository/UserRepository.php +++ b/www/app/Modules/Master/Repository/UserRepository.php @@ -13,15 +13,20 @@ final class UserRepository implements UserRepositoryInterface { public static function save(array $data): bool { - if(!isset($data['password']) || !isset($data['username']) || !isset($data['email'])) { - throw new InvalidArgumentException('Required (password, name, email) parameter is missing'); + if( + !isset($data['password']) || + !isset($data['username']) || + !isset($data['email']) || + !isset($data['role_id']) + ) { + throw new InvalidArgumentException('Required (password, name, email, role_id) parameter is missing'); } $user = new User; - $user->username = $data['username']; - $user->email = $data['email']; - $user->password = $data['password']; + foreach($data as $key => $value) { + $user->{$key} = $value; + } return $user->save(); } diff --git a/www/app/Modules/Profile/Controllers/ProfileController.php b/www/app/Modules/Profile/Controllers/ProfileController.php index 2167980..0289a60 100644 --- a/www/app/Modules/Profile/Controllers/ProfileController.php +++ b/www/app/Modules/Profile/Controllers/ProfileController.php @@ -97,7 +97,7 @@ function unflower(User $user): RedirectResponse function update(Request $request): RedirectResponse { $data = $request->validate([ - 'picture' => 'image|max:1004|nullable', + 'picture' => 'image|max:30004|nullable', 'description' => 'string|max:255|nullable', 'patreon' => 'string|url|nullable', 'github' => 'string|url|nullable', diff --git a/www/app/Modules/Profile/Repository/CommentRepository.php b/www/app/Modules/Profile/Repository/CommentRepository.php index f24c997..e9e8f70 100644 --- a/www/app/Modules/Profile/Repository/CommentRepository.php +++ b/www/app/Modules/Profile/Repository/CommentRepository.php @@ -23,15 +23,15 @@ public static function save(array $data): bool throw new \InvalidArgumentException('Required (user_id, post_id, description) parameters is missing'); } - if(!is_int($data['user_id']) || !is_int($data['post_id'])) { + if(!filter_var($data['user_id'], FILTER_VALIDATE_INT) || !filter_var($data['post_id'], FILTER_VALIDATE_INT)) { throw new \InvalidArgumentException('Required (user_id, post_id) is must be integer'); } $comment = new Comment; - $comment->user_id = $data['user_id']; - $comment->post_id = $data['post_id']; - $comment->description = $data['description']; + foreach($data as $key => $value) { + $comment->{$key} = $value; + } return $comment->save(); } diff --git a/www/app/Modules/Profile/Repository/PostRepository.php b/www/app/Modules/Profile/Repository/PostRepository.php index 5496ba4..ebaa8f1 100644 --- a/www/app/Modules/Profile/Repository/PostRepository.php +++ b/www/app/Modules/Profile/Repository/PostRepository.php @@ -28,10 +28,9 @@ public static function save(array $data): bool $post = new Post; - $post->file = $data['file']; - $post->name = $data['name']; - $post->description = $data['description']; - $post->user_id = $data['user_id']; + foreach($data as $key => $value) { + $post->{$key} = $value; + } return $post->save(); } @@ -41,7 +40,15 @@ public static function save(array $data): bool */ public static function remove(array $data): bool { - return true; + if(!isset($data['id'])) { + throw new \InvalidArgumentException('Required (id) parameter missing'); + } + + if(!is_int($data['id'])) { + throw new \InvalidArgumentException('Required (id) must be an integer'); + } + + return Post::query()->find($data['id'])->delete(); } /** @@ -49,7 +56,25 @@ public static function remove(array $data): bool */ public static function update(array $data): int { - return 0; + $query = Post::query(); + + if(!isset($data['id'])) { + throw new InvalidArgumentException('Required (id) parameter is missing'); + } + + $id = filter_var($data['id'], FILTER_VALIDATE_INT); + if ($id === false) { + throw new \InvalidArgumentException('id must be an integer'); + } + + $query = $query->where('id', $id); + + $updateData = []; + foreach($data as $key => $value) { + $updateData[$key] = $value; + } + + return $query->update($updateData); } /** diff --git a/www/database/factories/RoleFactory.php b/www/database/factories/RoleFactory.php new file mode 100644 index 0000000..58fc72b --- /dev/null +++ b/www/database/factories/RoleFactory.php @@ -0,0 +1,23 @@ + $this->faker->randomNumber(), + 'name' => $this->faker->name(), + 'slug' => $this->faker->slug(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]; + } +} diff --git a/www/database/migrations/2015_04_12_000000_create_orchid_users_table.php b/www/database/migrations/2015_04_12_000000_create_orchid_users_table.php deleted file mode 100644 index be1eb0b..0000000 --- a/www/database/migrations/2015_04_12_000000_create_orchid_users_table.php +++ /dev/null @@ -1,29 +0,0 @@ -string('name')->nullable(); - $table->jsonb('permissions')->nullable(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('users', function (Blueprint $table) { - $table->dropColumn(['name', 'permissions']); - }); - } -}; diff --git a/www/database/migrations/2015_10_19_214424_create_orchid_roles_table.php b/www/database/migrations/2015_10_19_214424_create_roles_table.php similarity index 86% rename from www/database/migrations/2015_10_19_214424_create_orchid_roles_table.php rename to www/database/migrations/2015_10_19_214424_create_roles_table.php index 6aded4f..e4551b1 100644 --- a/www/database/migrations/2015_10_19_214424_create_orchid_roles_table.php +++ b/www/database/migrations/2015_10_19_214424_create_roles_table.php @@ -12,10 +12,11 @@ public function up(): void { Schema::create('roles', function (Blueprint $table): void { - $table->increments('id'); + $table->id(); $table->string('slug')->unique(); $table->string('name'); - $table->jsonb('permissions')->nullable(); + $table->integer('permissions')->nullable(); + $table->timestamps(); }); } diff --git a/www/database/migrations/2015_10_19_214425_create_orchid_role_users_table.php b/www/database/migrations/2015_10_19_214425_create_orchid_role_users_table.php deleted file mode 100644 index 4228819..0000000 --- a/www/database/migrations/2015_10_19_214425_create_orchid_role_users_table.php +++ /dev/null @@ -1,38 +0,0 @@ -unsignedBigInteger('user_id'); - $table->unsignedInteger('role_id'); - $table->primary(['user_id', 'role_id']); - $table->foreign('user_id') - ->references('id') - ->on('users') - ->onUpdate('cascade') - ->onDelete('cascade'); - $table->foreign('role_id') - ->references('id') - ->on('roles') - ->onUpdate('cascade') - ->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('role_users'); - } -}; diff --git a/www/database/migrations/0001_01_01_000000_create_users_table.php b/www/database/migrations/2016_01_01_000000_create_users_table.php similarity index 90% rename from www/database/migrations/0001_01_01_000000_create_users_table.php rename to www/database/migrations/2016_01_01_000000_create_users_table.php index 80bc551..5e2bc49 100644 --- a/www/database/migrations/0001_01_01_000000_create_users_table.php +++ b/www/database/migrations/2016_01_01_000000_create_users_table.php @@ -19,10 +19,13 @@ public function up(): void $table->string('password'); $table->date('email_verified_at')->nullable(); $table->string('picture')->nullable(); - $table->string('description')->nullable(); + $table->text('description')->nullable(); $table->json('socialnetworks')->nullable(); + $table->integer('role_id'); + $table->foreign('role_id')->references('id')->on('roles'); + $table->rememberToken(); - + $table->timestamps(); }); diff --git a/www/database/migrations/2024_08_12_134940_create_posts_table.php b/www/database/migrations/2024_08_12_134940_create_posts_table.php index 23eeeca..69960ce 100644 --- a/www/database/migrations/2024_08_12_134940_create_posts_table.php +++ b/www/database/migrations/2024_08_12_134940_create_posts_table.php @@ -13,12 +13,12 @@ public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); - + $table->string('name'); - $table->string('description'); + $table->text('description'); $table->string('file'); - - + + $table->unsignedBigInteger('user_id'); $table->foreign('user_id') ->references('id') diff --git a/www/database/seeders/DatabaseSeeder.php b/www/database/seeders/DatabaseSeeder.php index 89582ce..551e687 100644 --- a/www/database/seeders/DatabaseSeeder.php +++ b/www/database/seeders/DatabaseSeeder.php @@ -6,6 +6,7 @@ use App\Modules\Profile\Models\Comment; use App\Modules\Profile\Models\Like; use App\Modules\Profile\Models\Post; +use Couchbase\Role; use Illuminate\Database\Seeder; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; @@ -19,13 +20,29 @@ public function run(): void { // User::factory(10)->create(); + \App\Modules\Master\Models\Role::create([ + 'name' => 'admin', + 'slug' => 'admin', + 'permissions' => 1 + ]); + + \App\Modules\Master\Models\Role::create([ + 'name' => 'user', + 'slug' => 'user', + 'permissions' => 3 + ]); + User::factory()->create([ 'username' => 'admin', 'email' => 'admin@admin.com', + 'password' => env('ADMIN_PASSWORD'), + 'role_id' => 1 ]); - Post::factory(10)->create(); - Comment::factory(30)->create(); - Like::factory(10)->create(); + if(env('APP_ENV') !== 'production') { + Post::factory(10)->create(); + Comment::factory(30)->create(); + Like::factory(10)->create(); + } } } diff --git a/www/resources/css/style.css b/www/resources/css/style.css index a9a7114..5d2044d 100644 --- a/www/resources/css/style.css +++ b/www/resources/css/style.css @@ -186,26 +186,59 @@ a.posts { background-color: #d9dbde; } -/*main {*/ -/* padding-top: 29px; !* Высота навбара по умолчанию *!*/ -/* flex: 1 0 auto; !* Растягиваем контент *!*/ -/*}*/ - -/*html, body {*/ -/* height: 100%;*/ -/* margin: 0;*/ -/*}*/ - -/*body {*/ -/* display: flex;*/ -/* flex-direction: column;*/ -/* min-height: 150vh;*/ -/*}*/ - -/*footer {*/ -/* flex-shrink: 0; !* Фиксируем футер *!*/ -/* margin-top: auto; !* Прижимаем вниз *!*/ -/*}*/ +/* Admin panel */ +.sidebar { + min-height: calc(100vh - 56px); + background-color: #f8f9fa; + border-right: 1px solid #dee2e6; +} +.sidebar .nav-link { + color: #333; + border-radius: 0; +} +.sidebar .nav-link:hover { + background-color: #e9ecef; +} +.sidebar .nav-link.active { + background-color: #0d6efd; + color: white; +} +.content-header { + background-color: #f8f9fa; + padding: 15px 0; + margin-bottom: 20px; + border-bottom: 1px solid #dee2e6; +} +.stats-card { + transition: transform 0.2s; +} +.stats-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} +.navbar-brand { + font-weight: 600; +} +.table-actions { + white-space: nowrap; + width: 100px; +} + +.cell-text { + max-width: 200px; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; +} + +.json-block { + max-width: 400px; + max-height: 200px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; +} + @media (max-width: 1200px) { .responsive-element { diff --git a/www/resources/js/script.js b/www/resources/js/script.js index 39e03da..ff39d57 100644 --- a/www/resources/js/script.js +++ b/www/resources/js/script.js @@ -15,4 +15,165 @@ document.addEventListener('DOMContentLoaded', function(){ }); }); + if(window.location.href.includes('/admin')) { + const uploadFile = document.getElementById('uploadFile'); + const previewImage = document.getElementById('previewImage'); + const removeImage = document.getElementById('removeImage'); + const uploadContainer = document.getElementById('uploadContainer'); + const filepath = document.getElementById('filepath'); + + if (uploadFile && previewImage && removeImage) { + uploadFile.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = function(e) { + previewImage.src = e.target.result; + previewImage.style.display = 'block'; + removeImage.style.display = 'block'; + }; + reader.readAsDataURL(file); + } + }); + + removeImage.addEventListener('click', function() { + uploadFile.value = ''; + previewImage.src = ''; + filepath.value = ''; + previewImage.style.display = 'none'; + removeImage.style.display = 'none'; + }); + + // Обработка перетаскивания файла + uploadContainer.addEventListener('dragover', function(e) { + e.preventDefault(); + this.classList.add('border-primary'); + this.classList.remove('border-dashed'); + }); + + uploadContainer.addEventListener('dragleave', function(e) { + e.preventDefault(); + this.classList.remove('border-primary'); + this.classList.add('border-dashed'); + }); + + uploadContainer.addEventListener('drop', function(e) { + e.preventDefault(); + this.classList.remove('border-primary'); + this.classList.add('border-dashed'); + + if (e.dataTransfer.files.length) { + uploadFile.files = e.dataTransfer.files; + filepath.value = e.dataTransfer.files; + const event = new Event('change'); + uploadFile.dispatchEvent(event); + } + }); + } + + + const keyValueContainer = document.getElementById('keyValueContainer'); + const addKeyValueBtn = document.getElementById('addKeyValueBtn'); + const objectField = document.getElementById('objectField'); + + function addKeyValuePair(key = '', value = '') { + const pairId = Date.now() + Math.random().toString(36).substr(2, 5); + const pairElement = document.createElement('div'); + pairElement.className = 'card mb-2 key-value-pair'; + pairElement.id = `pair-${pairId}`; + pairElement.innerHTML = ` +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ `; + keyValueContainer.appendChild(pairElement); + + const removeBtn = pairElement.querySelector('.remove-pair-btn'); + removeBtn.addEventListener('click', function() { + pairElement.remove(); + updateObjectField(); + }); + + const keyInput = pairElement.querySelector('.key-input'); + const valueInput = pairElement.querySelector('.value-input'); + + keyInput.addEventListener('input', updateObjectField); + valueInput.addEventListener('input', updateObjectField); + } + + function updateObjectField() { + const pairs = document.querySelectorAll('.key-value-pair'); + const data = {}; + + pairs.forEach(pair => { + const keyInput = pair.querySelector('.key-input'); + const valueInput = pair.querySelector('.value-input'); + + if (keyInput.value.trim() !== '') { + data[keyInput.value] = valueInput.value; + } + }); + + objectField.value = JSON.stringify(data); + } + + addKeyValueBtn.addEventListener('click', function() { + addKeyValuePair(); + }); + + // Загрузка существующих данных + try { + const existingData = JSON.parse(objectField.value); + if (existingData && typeof existingData === 'object') { + Object.keys(existingData).forEach(key => { + addKeyValuePair(key, existingData[key]); + }); + } + + if (Object.keys(existingData).length === 0) { + addKeyValuePair(); + } + } catch (e) { + console.error('error unserialze JSON:', e); + addKeyValuePair(); + } + + document.getElementById('objectForm').addEventListener('submit', function(e) { + updateObjectField(); + + const pairs = document.querySelectorAll('.key-value-pair'); + let hasErrors = false; + + pairs.forEach(pair => { + const keyInput = pair.querySelector('.key-input'); + const valueInput = pair.querySelector('.value-input'); + + if (keyInput.value.trim() === '' && valueInput.value.trim() !== '') { + keyInput.classList.add('is-invalid'); + hasErrors = true; + } else { + keyInput.classList.remove('is-invalid'); + } + }); + + if (hasErrors) { + e.preventDefault(); + } + }); + } + }); diff --git a/www/resources/views/admin/content/detail.blade.php b/www/resources/views/admin/content/detail.blade.php new file mode 100755 index 0000000..285108d --- /dev/null +++ b/www/resources/views/admin/content/detail.blade.php @@ -0,0 +1,122 @@ +@extends('app.admin') + +@section('content') +
+
+ @csrf + @method($method) + @if(request()->has('id')) + + @endif +
+
+ @if(request()->has('id')) +
+
ID: {{ request()->get('id') }}
+
+ @endif + @foreach($items as $item) + @switch($item['type']) + @case('number') +
+ + +
+ @break + @case('textarea') +
+ + +
+ @break + @case('text') + @if(!in_array($item['column'], ['picture', 'file',])) +
+ + +
+ @else +
+

{{ $item['column'] }}

+ + +
+ Preview Image + + +
+ + +
+ + + +
+ +

Upload file

+

Перетащите или кликните для выбора

+
+
+
+ @endif + @break + @case('datetime') +
+ + +
+ @break + @case('object') +
+ + +
+
+ + + + +
+ @break + @endswitch + @endforeach +
+
+ +
+
+ + Cancel + + +
+
+
+
+@endsection diff --git a/www/resources/views/admin/content/index.blade.php b/www/resources/views/admin/content/index.blade.php new file mode 100755 index 0000000..b6e4b1d --- /dev/null +++ b/www/resources/views/admin/content/index.blade.php @@ -0,0 +1,105 @@ +@extends('app.admin') + +@php + $fillables = isset($contents) && $contents->count() > 0 ? array_keys($contents[0]->toArray()) : $fillables; +@endphp + +@section('content') + + +
+ + Create new element + +
+ + +
+
+
+ + + + @foreach($fillables as $fillable) + + @endforeach + + + + + @if(isset($contents) && $contents->count() > 0) + + @foreach($contents as $content) + + @foreach($content->toArray() as $fillable => $column) + + @endforeach + + + + @endforeach + + @endif +
+ {{ $fillable }} + Actions
+
+ @if(is_array($column) || is_object($column)) +
+ + Данные по обратной связи + +
+                                                        {{ json_encode($column, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) }}
+                                                    
+
+ @else + @if(in_array($fillable, $fillables)) + {{ $column ?? '' }} + @endif + @endif +
+
+ +
+
+
+
+ + + @if(isset($contents) && $contents->count() > 0) +
+ +
+ @endif + +@endsection diff --git a/www/resources/views/admin/index.blade.php b/www/resources/views/admin/index.blade.php new file mode 100755 index 0000000..26e6da1 --- /dev/null +++ b/www/resources/views/admin/index.blade.php @@ -0,0 +1,18 @@ +@extends('app.admin') + +@section('content') +
+
+
+
Sum users: {{ $usersCount }}
+
+
+
+
Sum posts {{ $postsCount }}
+
+
+
+
Sum comments {{ $commentCount }}
+
+
+@endsection diff --git a/www/resources/views/admin/login/index.blade.php b/www/resources/views/admin/login/index.blade.php new file mode 100755 index 0000000..630d269 --- /dev/null +++ b/www/resources/views/admin/login/index.blade.php @@ -0,0 +1,72 @@ +@extends('app.admin') + +@section('content') +
+
+
+
+ + + +
+

+ Авторизация в панели управления +

+
+ + @if($errors->any()) +
+
+
+ + + +
+
+

+ Ошибка авторизации +

+
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+
+
+
+ @endif + +
+ @csrf +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+@endsection diff --git a/www/resources/views/app/admin.blade.php b/www/resources/views/app/admin.blade.php new file mode 100644 index 0000000..42de74c --- /dev/null +++ b/www/resources/views/app/admin.blade.php @@ -0,0 +1,90 @@ + + + + + + + Admin Panel + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + +
+
+ + +
+ @if($errors->any()) + @foreach($errors->all() as $error) + + @endforeach + @endif + + @if(Session::has('success')) + + @endif + + + @if(\Illuminate\Support\Facades\Auth::check() && (isset($exception) && $exception->getStatusCode() < 400)) +
+

@yield('title')

+ + Выйти + +
+ @endif + + @yield('content') +
+
+
+ + + + + diff --git a/www/routes/web.php b/www/routes/web.php index a017997..0362dc0 100644 --- a/www/routes/web.php +++ b/www/routes/web.php @@ -4,6 +4,7 @@ $auth = new \App\Modules\Auth\Auth; $profile = new \App\Modules\Profile\Profile; $search = new \App\Modules\Search\Search; +$admin = new \App\Modules\Admin\Admin; $master->registerRoutes() ->enable(); @@ -16,3 +17,6 @@ $search->registerRoutes() ->enable(); + +$admin->registerRoutes() + ->enable();