Version 16 (modified by 3 years ago) ( diff ) | ,
---|
Имплементација
Организација на контролата врз верзии на изворниот код
За контрола врз верзиите на изворниот код ќе се користи алатката Git.
Изворен код
Имплементациски дијаграм
Најава и автентикација
Во овој дел ќе бидат прествени и објаснати одредени функционалности од системот кои се користат нај често во рамки на системот. Ќе започнам со објаснување за начинот на креирање на оддел кој што е предуслов за креирање на фолдер т.е. документ.
Во MVC системот моделот е класа која се состои од податоци запишани во одредена табела во база. View го претставува интерфејсот преку кој корисникот комуницира со апликацијата. Кога корисникот презема акција, Controller се справува со акцијата и ги ажурира податоците во база преку моделот доколку е потребно.
Во Laravel, моделот обично е класа со променливи што одговараат на колоните во базата на податоци т.е. променливите кои се дефинирани во Migrations во одредена табела. Контролерите се одговорни за завршување на корисничките дејства и за управување со деловната логика на апликацијата. Рутирањето на ресурсите во Laravel ги доделува „CRUD“ операциите или функциите дефинирани во контролерот со една линија код.
Ќе започнам со објаснување на начинот на најва и автентикација во апликацијата. За автентикација се користи Middleware кој што обезбедува механизам за проверка и филтрирање на HTTP барањата на апликацијата. Laravel вклучува софтвер кој потврдува дека корисникот е автентициран. Ако корисникот не е автентициран, ќе го пренасочи корисникот на екранот за најава. Меѓутоа, ако корисникот е автентициран, ќе дозволи барањето да продолжи да се процесира. Освен базичните класи за автентикација кои ни ги овозможува Laravel во Middleware директориумот ги додаваме сите проверки кои му дозволуваат на корисникот да се автентицира. На пример во апликацијата предуслов за најава е да се креира лозинка за да се активира профилот на корисникот. Laravel го олеснува начинот на заштитата на апликацијата од напади за не верифицирани барања меѓу страници со помош на CSRF. Laravel автоматски генерира CSRF „токен“ за секоја активна корисничка сесија управувана од апликацијата. Овој токен се користи за да се потврди дека автентицираниот корисник е тој што всушност ги поставува барањата до апликацијата. Во секоја форма преку која праќаме HTTP барање потребно е да се додаде и CSFR токенот.
Route::group(['prefix' => 'auth'], function () { Route::get('/login', "Auth\LoginController@showLogin")->name("auth.showLogin"); Route::get('/verify-login/{id}/{token}', "Auth\VerifyLoginController@index")->middleware("CheckVerifyToken")->name("verify-login.index"); Route::post('/verify-login/{id}/{token}', "Auth\VerifyLoginController@verify")->name("verify-login"); Route::post('/login', "Auth\LoginController@login")->name("auth.login"); Route::post('/logout', "Auth\LoginController@logout")->name("auth.logout"); Route::get('/forgot', "Auth\ForgotPasswordController@showForgotPassword")->name("auth.forgotShow"); Route::post('/forgot', "Auth\ForgotPasswordController@forgotPassword")->name("auth.forgot"); Route::group(['middleware' => "createPassword"], function () { Route::get('/create-password/{id}/{token}', "Auth\CreatePasswordController@showCreatePassword")->name("auth.create-password-show"); Route::post('/create-password/{id}/{token}', "Auth\CreatePasswordController@createPassword")->name("auth.create-password"); }); });
Листа на рути за автентикација
Како што може да се види за секој повик за автентикација потребен е параметарот token кој што означува уникатен идентификатор на корисничкиот профил преку кој корисникот се автентицира и праќа барања до серверот.
public function __construct() { $this->middleware("guest")->except('logout'); } public function showLogin() { return view("auth.login"); } public function login(LoginRequest $request, Hashid $hashid) { $user = User::whereUsername($request->username)->first(); if (is_null($user)) { Alert::flash("Please check your credentials", "error"); return redirect()->route("auth.login"); } if (!$user->is_active) { Alert::flash("Your account is blocked or its not confirmed yet. Please contact with your system administrator or check your email.", "error"); return redirect()->route("auth.login"); } if (!Hash::check($request->password, $user->password)) { Alert::flash("Your password is incorrect", "error"); return redirect()->route("auth.login"); } if ($user->is_forgot_password) { $user->is_forgot_password = false; } $user->security_code = rand(10000, 99999); $user->verify_token = Str::uuid(); $user->is_online = true; $user->save(); $user->notify(new VerifyUser($user)); return redirect()->route("verify-login.index", [ "id" => $hashid->encode($user->id), "token" => $user->verify_token ]); }
Методи од контролерот LoginController за најава во апликацијата
Со помош на методот showLogin() го прикажуваме интерфејсот за најава во апликацијата. Во методот login() се врши најавата. Во овој метод има два параметри од кои едниот е валидацијата на податоците преку барањето која се врши преку класата LoginRequest како и $hashid од класата HashId која ни помага при енкодирање на токенот за верификација на најавата. Во овој метод проверуваме дали корисникот постои во база, е активен т.е. дали креирал лозинка и се најавил во системот и дали има внесено точна лозинка. Потоа му се генеира безбедносен код кој што ќе му биде испратен на маил за верификација на најавата преку VerifyUser класата за праќање на нотификација како и токенот за верификација преку кој ќе се најави. По автентикација на корисникот ќе му се појави екранот за внес на кодот за верификација т.е. ќе се прати барање до контролерот VerifyLoginController.
public function toMail($notifiable) { $hashId = new Hashid(); return (new MailMessage) ->greeting("Login verification") ->line("To verify click the button and enter your security code.") ->line("Your security code is: " . $this->user->security_code) ->action("Verify", route("verify-login.index", [ "id" => $hashId->encode($this->user->id), "token" => $this->user->verify_token ])); }
Метод од класата VerifyUser за праќање на верификацистиот код на маил
public function index() { return view("auth.verify-login"); } public function verify(Request $request, Hashid $hashid, $id, $token) { $user = User::findOrFail($hashid->decode($id)); if($request->code != $user->security_code) { Alert::flash("Security code is wrong", "error"); return redirect()->back(); } auth()->login($user); return redirect()->route("dashboard.index"); }
Методи од контролерот VerifyLoginController
Секогаш почнуваме со приказ на интерфејсот т.е. index методот која ни служи за приказ на целата колекција од податоци. Откако корисникот ќе се најави ќе стигне барањето до методот verify кој ќе го прими барањето, ќе направи декодирање на претходно енкодираниот $hashid и ќе му дозволи на корисникот да се најави во системот.
Управување со оддели
class Department extends Model { use HasFactory; protected $table = "departments"; protected $fillable = ["name", "code", "location", "user_id", "no_of_folders"]; protected $casts = [ 'created_at' => 'datetime:d-m-Y', ]; public function folder(){ return $this->hasMany(Folder::class); } public function getCreatedByName() { return User::where('id', $this->user_id)->pluck('username')->first(); } }
Department моделот
Во горенаведениот код се запишани променливите кои претходно биле дефнирани во табелата Departments. Исто така во овој дел се дефинираат и релации помеѓу два или повеке модели. Во конкретниот случај е дефинирана релација помеѓу Department и Folder моделот каде што се означува дека еден оддел може да има неколку фолдери. Може да се дефинираат и методи кои влечат одредени податоци од истиот модел како што методот getCreatedByName() кој го наоѓа корисничкото име од User модел-от и истиот може потоа да се прикаже во интерфејс на апликацијата преку класата Department.
public function index() { return view("dashboard.departments.index")->with([ "departments" => Department::all(), ]); }
Метод од контролерот DepartmentsController за приказ на сите оддели запишани во база
Методот index се повикува кога рутата /departments е вчитана со методот GET HTTP. Во овој метод, ние ги преземаме сите достапни податоци во табелата за оддели користејќи го моделот Department и го пренесуваме во интерфејсот како променлива. Ова значи дека во интерфејсот, променливата $departments ќе може да се користи за да се прикажат податоци од колекцијата која ги состои сите оддели.
// Departments view Route::group(['middleware' => 'permission:view_all_departments'], function () { Route::get("/departments", "Dashboard\DepartmentsController@index")->name("dashboard.departments.index"); }); // Departments manage Route::group(['middleware' => 'permission:manage_all_departments'], function () { Route::post("/departments/store", "Dashboard\DepartmentsController@store")->name("dashboard.departments.store"); Route::patch("/departments/{id}/edit", "Dashboard\DepartmentsController@edit")->name("dashboard.departments.edit"); });
Листа на рути за оддели
Секој повик во Laravel потребно е да има посебна рута. Рутата со име index ја дефинирав посебно за да се ограничи пристапот за корисници кои имаат ролја во која не е дозволено да управуваат со оддели. Рутата store служи за креирање на нов оддел. Методот во оваа рута секогаш треба да е post затоа што се запишуваат нови податоци т.е. се креира нов оддел. Методот patch се повикува кога се прави ажурирање на податоците во база за веќе креиран оддел што во овој случај е рутата со име edit. На двете рути за да прикажат и ажурираат оддел потребно им е да се додели вредност id која според id-то во база ќе го најде конкретниот оддел.
public function store(NewDepartmentRequest $request) { $department = new Department(); $department->name = $request->name; $department->code = $request->code; $location = 'Departments' . DIRECTORY_SEPARATOR . $request->code; if(!Storage::disk('local')->has($location)){ Storage::disk('local')->makeDirectory($location); } $department->location = $location; $department->user_id = auth()->id(); $department->save(); Alert::flash("New Department added successfully", "success"); return redirect()->route("dashboard.departments.index"); }
Метод од контролерот DepartmentsController за креирање на нов оддел
Како што може да се види и во кодот методот како влезен параметар има објект од класата NewDepartmentRequest каде што се прави валидацијата на променливите од моделот Department кога тие ќе стигнат преку HTTP барањето преку формата која претходно ја пополнувал корисникот во интерфејсот на апликацијата. Почнуваме со креирање на нов објект од класата Department и сите параметри кои треба да се запишат во база за тој оддел ќе бидат еднакви на тие што доаѓаат од повикот. При креирање на одделот за истиот креираме и фолдер во сервер за зачувување на датотеките. Името на фолдер-от го одредува кодот кој го доделуваме на новиот оддел а локацијата на фолдер-от ја зачувуваме во променлива. На крај го зачувуваме одделот и прикажуваме порака на корисникот дали во таа сесија поминало барањето или не. Доколку има грешки се прикажуваат валидациски пораки. Последен чекор е да се вратиме на почетниот екран.
class Alert { public static function flash($message, $type = "success") { return request()->session()->flash("alert", [ "message" => $message, "type" => $type ]); } }
Класата Alert за приказ на пораки од сесија
Методот во класата алерт е креиран за да му ја олесни работата на корисникот во моментот кога нема да помине барањето поради грешка при валидација на податоците или друг проблем. За приказ на пораките користев Тоаst JS [11].
{{-- Validation errors --}} @if($errors->any()) @foreach ($errors->all() as $error) <script> new Toast({ message: "{{$error}}", type: 'danger' }); </script> @endforeach @endif {{-- Http request alerts --}} @if(session()->has("alert")) <script> new Toast({ message: "{{session()->get("alert.message")}}", type: '{{session()->get("alert.type")}}' }); </script> @endif
Интерфејсот за приказ на пораките
Во првиот дел проверуваме дали има грешка и ја враќаме пораката од сесија и типот го поставуваме да е секогаш грешка за да биде соодветно на пораката додека во вториот дел проверувам дали во сесија има објект alert т.е. дали се враќа одговор од серверот и ја прикажуваме пораката и типот на одговорот според сесија.
public function edit(UpdateDepartmentRequest $request, $id) { $department = Department::findOrFail($id); $folders = $department->folder; $oldLocation = DIRECTORY_SEPARATOR . 'Departments' . DIRECTORY_SEPARATOR . $department->code; $department->name = $request->name; $department->code = $request->code; $department->updated_at = Carbon::now(); if($department->isDirty('code')) { $location = 'Departments' . DIRECTORY_SEPARATOR . $request->code; if(!Storage::disk('local')->has($location)){ Storage::disk('local')->move($oldLocation, $location); $department->location = $location; } foreach ($folders as $folder) { $currArchId = explode('/', $folder->arch_id)[1]; $folder->arch_id = $department->code . '/' . $currArchId; $folder->save(); foreach($folder->files as $file) { $file->location = $location . DIRECTORY_SEPARATOR . $folder->name . DIRECTORY_SEPARATOR . $file->name; $file->save(); } } } $department->save(); Alert::flash("Department edited successfully"); return redirect()->route("dashboard.departments.index"); }
Метод од контролерот DepartmentsController за уредување на оддел
Со помош на методот edit може да се смени името на одделот или кодот. Во случај кога ќе се прави измена во кодот потребно е името на фолдерот каде што се наоѓаат сите датотеки од тој оддел да се преименува. Со таа цел преку функцијата isDirty() со која може да се дознае дали моделот е уреден пред да се запише во база проверувам дали има промена и доколку има прво го поместуваме одделот во нов директориум а потоа во две итерации ги изминуваме сите фолдери и датотеки и нивните патеки во база ги заменуваме со новата. На крај ги зачувуваме и прикажувам соодветна порака за извршена акција како и го враќам корисникот во страницата со сите оддели.
public function destroy($id) { $department = Department::find($id); //$department->delete(); $folders = $department->folder()->count(); if($folders > 0){ Alert::flash($department->name . " has " . $folders . " document/s associated", "error"); } else { $department->delete(); Alert::flash($department->name . " deleted successfully"); } return redirect()->route("dashboard.departments.index"); }
Метод од контролерот DepartmentsController за бришење на оддел
Покрај уредување на оддел корисникот со привилегии администратор или референт има можност да избрише еден оддел што значи да се одстранат сите записи од база како и сите фолдери и датотеки за истиот. Корисникот има можност да го избрише одделот само ако тој оддел е празен т.е. доколку во него не постои ниту еден фолдер. Во спротивно, се фрла порака дека тој оддел има асоцирани фолдери со него и не ја дозволува акцијата да се изврши.
public function downloadDepartment($id) { $department = Department::find($id); $folders = Folder::where('department_id', $id)->get(); $flag = false; foreach($folders as $folder){ if($folder->no_of_files > 0) $flag=true; break; } if($flag) { $zip_file=Storage::disk('local')->path('Department.zip'); $zip = new \ZipArchive(); $zip->open($zip_file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); $path = Storage::disk('local')->path($department->location); $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path)); foreach ($files as $file) { if (!$file->isDir()) { $filePath = $file->getRealPath(); $relativePath = substr($filePath, strlen($path) + 1); $zip->addFile($filePath, $relativePath); } } $zip->close(); $headers = array('Content-Type' => 'application/octet-stream'); $zip_new_name = Carbon::now()->format('d.m.Y - ') . $department->name . '.zip'; return response()->download($zip_file, $zip_new_name, $headers); } else { Alert::flash("This department has no files", "warning"); return redirect()->back(); } }
Метод од контролерот DepartmentsController за симнување на фолдери и датотеки од оддел
За потребите на компанијата додаден е метод за симнување на сите фолдери со нивните датотеки од еден оддел преку кој се креира zip кој ги содржи сите фолдери од еден оддел со истата структура како што се тие запишани на сервер. На почеток проверуваме дали има празни фолдери со цел тие да ги игнорираме затоа што не содржат ниту една датотека.Класата RecursiveIteratorIterator која што е тип на итератор кој ни овозможува рекурзивно да ги поминеме сите патеки ми помогна да ги најдам потребните фолдери и датотеки. За разлика од обичен итератор кој што пребарува низ листа RecursiveIteratorIterator може да се каже дека пребарува низ дрво и ја олеснува работата кога се работи за неколку патеки во еден директориум. За секоја пронајдена датотека ја добиваме патеката од Storage и ја додаваме во zip. На крај ја симнуваме датотеката со доделено име, прикажуваме соодветна порака и се враќаме назад. Во овој дел клуч е патеката која ја запишувам при креирање на оддел. На истиот начин направив и функција за симнување на сите оддели со нивните фолдери и датотеки при што се игнорираат празните оддели и фолдери.
Корисници и уредување на кориснички податоци
Секој корисник има можност да ги промени своите податоци за профилот како и администраторот има можност да ги промени податоците за секој корисник и да додаде нови. Исто така секој корисник има можност да ги менува е-адресата, корисничкото име или лозинката. Дополнително администраторот има можност да постави валидациски правила за прикачување на нови датотеки. Ќе започнам со објаснување на контролерот UsersController преку кој се креираат нови корисници или се менуваат кориснички податоци.
Route::group(['middleware' => 'permission:view_all_users'], function () { Route::get("/users", "Dashboard\UsersController@index")->name("dashboard.users.index"); }); Route::group(['middleware' => 'permission:manage_all_users'], function () { Route::post("/users/store", "Dashboard\UsersController@store")->name("dashboard.users.store"); Route::patch("/users/{id}/edit", "Dashboard\UsersController@edit")->name("dashboard.users.edit"); Route::patch("/users/{id}/block", "Dashboard\UsersController@block")->name("dashboard.users.block"); Route::patch("/users/{id}/unblock", "Dashboard\UsersController@unblock")->name("dashboard.users.unblock"); Route::delete("/users/{id}/destroy", "Dashboard\UsersController@destroy")->name("dashboard.users.destroy"); });
Листа на рути за управување со корисници
Рутата за приказ на корисници е посебно дефинирана со цел да се овозможи пристап за корисници од различни ролји т.е. референтот да има можност да ја види листата со корисници но да нема можност да управува со нив.
public function index() { return view("dashboard.users.index")->with([ "users" => User::all(), "roles" => Role::all() ]); } public function store(NewUserRequest $request) { $user = new User(); $user->name = $request->name; $user->surname = $request->surname; $user->email = $request->email; $user->phone_number = $request->phone_number; $user->username = $request->username; $user->password = $user->generateTemporaryPassword(); $user->security_code = $user->generateSecurityCode(); $user->verify_token = $user->generateVerifyToken(); $user->role_id = $request->userRole; if ($request->hasFile("avatar")) { $image = $request->file("avatar"); $avatarName = $user->id . '_avatar' . time() . '.' . request()->avatar->getClientOriginalExtension(); if (!empty($user->avatar)) { Storage::disk('uploads')->delete("/users/" . $user->avatar); } Storage::disk('uploads')->put("/users/" . $avatarName, File::get($image)); $user->avatar = $avatarName; } $user->created_by = auth()->user()->id; $user->save(); $user->notify(new WelcomeUser($user)); Alert::flash("New user added successfully"); return redirect()->route("dashboard.users.index"); }
Методи на контролерот UsersController за приказ и креирање на корисници
Додавање на нов корисник оди преку методот store кој како параметар го прима барањето кое ќе го валидира според правилата дефинирани во NewUserRequest класата. Некои од податоците за кои не им се доделува вредност при креирање креирав методи во User моделот за генерирање на random вредности со цел за да помине валидацијата на тие задолжителни полиња како на пример што е generateTemporaryPassword() методот. За корисникот исто така може да се додаде слика која ја зачувуваме во сервер. На крај се зачувува корисникот и се праќа нотификација од класата WelcomeUser() преку која праќаме маил до новиот корисник за креирање на нова лозинка со што корисникот ќе има активен профил и ќе може да се најави. На сличен начин работи и контролерот за уредување на кориснички податоци. За разлика од креирање во тој дел доколку се промени е-адресата на корисникот се генеира нов токен за верификација како и безбедносен код и се праќаат до корисникот со цел да го верифицира истиот и да може да се најави пак во системот.
if($user->isDirty('email')) { $user->is_active = false; $user->security_code = $user->generateSecurityCode(); $user->verify_token = $user->generateVerifyToken(); $user->notify(new VerifyNewEmail($user)); }
Дел од методот за уредување на корисник (промена на е-адреса)
public function block(Request $request, $id) { $user = User::find($id); $user->is_active = false; $user->save(); Alert::flash($user->name . " User blocked successfully"); return redirect()->route("dashboard.users.index"); }
Методот за блокирање на корисник
Доколку еден корисник не работи веќе во фирмата тоа значи дека истиот не смее да се најави веќе во системот. Со таа намена методот block го менува статусот на корисникот од активен во не активен и не му дозволува да се најави во системот. На ист начин може и да се деблокира еден корисник.
public function editUserData(UpdateUserData $request, $id) { $user = User::findOrFail($id); $user->name = $request->name; $user->surname = $request->surname; $user->phone_number = $request->phone_number; if ($request->hasFile("avatar")) { $image = $request->file("avatar"); $avatarName = $user->id . '_avatar' . time() . '.' . request()->avatar->getClientOriginalExtension(); if (!empty($user->avatar)) { Storage::disk('uploads')->delete("/users/" . $user->avatar); } Storage::disk('uploads')->put("/users/" . $avatarName, File::get($image)); $user->avatar = $avatarName; } $user->save(); Alert::flash("User data updated successfully"); return redirect()->route("dashboard.settings.index"); }
Метод за промена на личните кориснички податоци
Преку методот editUserData секој најавен корисник има можност да си ги менува своите лични кориснички податоци како име, презиме, мобилен број или слика.
public function destroy(Request $request, $id) { $user = User::find($id); if(Folder::where('user_id', $user->id)->count() == 0) { if (Storage::disk("uploads")->exists("users" . DIRECTORY_SEPARATOR . $user->avatar)) { Storage::disk("uploads")->delete("users" . DIRECTORY_SEPARATOR . $user->avatar); } $user->delete(); Alert::flash($user->name . " deleted successfully"); return redirect()->route("dashboard.users.index"); } else { Alert::flash($user->name . "has associated folders"); return redirect()->back(); } }
Метод за бришење на корисник
Администраторот на апликацијата има можност да избрише корисник со користење на методот destroy. При бришење во истиот момент је отстранува и сликата од профилот на корисникот од сервер како и сите податоци за него во база. Притоа, еден корисник може да се избрише само доколку нема асоцирани фолдери во него т.е. доколку тој корисник никогаш не додал фолдери и датотеки во одреден оддел.
public function updateUsername(UsernameSettingsRequest $request) { $user = auth()->user(); $user->username = $request->username; $user->updated_at = Carbon::now(); $user->save(); Alert::flash("Username updated successfully"); auth()->logout(); session()->flush(); return redirect()->route("auth.showLogin"); }
Метод за промена на корисничко име во SettingsController контролерот
Преку методот updateUsername корисникот има можност да го промени своето корисничко име. Методот како параметар го добива барањето кое ќе биде валидирано според правилата поставени во UsernameSettingsRequest. По завршена акција корисникот се одјавува. Доколку се промени е-адресата тој исто така добива и маил за потврда на новата со цел да може да се најави пак. На ист начин работи и методот за промена на лозинка.
public function fileTypes(FileTypeRequest $request) { $fileType = FileType::find("1"); $fileType->mimes = $request->mimes; $fileType->max_size = $request->max_size; $fileType->user_id = auth()->id(); $fileType->updated_at = Carbon::now(); if(auth()->user()->hasPermission("manage_file_types")) { $fileType->save(); Alert::flash("File validations updated successfully"); return redirect()->back(); } else { Alert::flash("You don't have permission to change file validations", "error"); return redirect()->back(); } }
Метод за промена на валидациски правила во SettingsController контролерот
Валидациските правила се зачувуваат во база во еден ред од посебна табела и истите потоа се превземаат при валидација за прикачување на датотеки. Администраторот има можност да ги дефинира сите дозволени типови на датотеки кои можат да се прикачат како и максималната големина по датотека.
$mimes = FileType::find("1")->mimes; $maxSize = FileType::find("1")->max_size; if ($this->isMethod("patch")) { $fileRules = [ "file_item.*" => "mimes:{$mimes}|max:{$maxSize}" ]; } else { $fileRules = [ "file_item.*" => "mimes:{$mimes}|max:{$maxSize}" ]; }
Дел од кодот за валидација на датотеки Во овој дел од FolderRequest класата за валидација на нов фолдер може да се забележи дека во mimes се запишуваат типови на датотеки кои што администраторот претходно ги дефинирал преку методот fileTypes(). Прво го наоѓаме првиот ред од база од класата FileType и неговите податоци ги поставуваме како валидациски правила.
===Управување со фолдери и датотеки
Attachments (1)
- project_structure.png (61.6 KB ) - added by 3 years ago.
Download all attachments as: .zip