diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore index a76f485..cf48d1a 100644 --- a/apps/backend/.gitignore +++ b/apps/backend/.gitignore @@ -21,5 +21,4 @@ /vendor Homestead.json Homestead.yaml -Thumbs.db -composer.lock \ No newline at end of file +Thumbs.db \ No newline at end of file diff --git a/apps/backend/app/Http/Controllers/AiRatingController.php b/apps/backend/app/Http/Controllers/AiRatingController.php new file mode 100644 index 0000000..9ac20fd --- /dev/null +++ b/apps/backend/app/Http/Controllers/AiRatingController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateAiRatingRequest $request, string $id) + { + $item = AiRating::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + AiRating::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/AppController.php b/apps/backend/app/Http/Controllers/AppController.php new file mode 100644 index 0000000..66b932d --- /dev/null +++ b/apps/backend/app/Http/Controllers/AppController.php @@ -0,0 +1,36 @@ +isOwnedByUser()) { + return response()->json(['message' => 'Forbidden'], 403); + } + + $project->load([ + 'chapters.sections' => function ($query) { + $query->with([ + 'children.textBlocks', + 'children.tables', + 'children.images', + 'children.children', + 'textBlocks', + 'tables', + 'images', + ]); + }, + ]); + + + return response()->json($project); + } +} \ No newline at end of file diff --git a/apps/backend/app/Http/Controllers/AuthController.php b/apps/backend/app/Http/Controllers/AuthController.php index ebd8da9..ebb454a 100644 --- a/apps/backend/app/Http/Controllers/AuthController.php +++ b/apps/backend/app/Http/Controllers/AuthController.php @@ -11,6 +11,16 @@ class AuthController extends Controller { + private function getRole() + { + $user = Auth::user(); + if (!$user) { + return response()->json(['error' => 'User not found'], 404); + } + + $role = $user->getRoleNames(); + return response()->json(['role' => $role]); + } public function register(Request $request) { $request->validate([ @@ -33,10 +43,13 @@ public function register(Request $request) return response()->json(['error' => 'Could not create token'], 500); } - return response()->json([ - 'token' => $token, - 'user' => $user, - ], 201); + return response()->json( + [ + 'token' => $token, + 'user' => $user, + ], + 201, + ); } public function login(Request $request) @@ -44,7 +57,7 @@ public function login(Request $request) $credentials = $request->only('email', 'password'); try { - if (!$token = JWTAuth::attempt($credentials)) { + if (!($token = JWTAuth::attempt($credentials))) { return response()->json(['error' => 'Invalid credentials'], 401); } } catch (JWTException $e) { @@ -75,7 +88,12 @@ public function getUser() if (!$user) { return response()->json(['error' => 'User not found'], 404); } - return response()->json($user); + $role = $user->getRoleNames(); + + return response()->json([ + 'user' => $user, + 'role' => $role + ]); } catch (JWTException $e) { return response()->json(['error' => 'Failed to fetch user profile'], 500); } diff --git a/apps/backend/app/Http/Controllers/ChapterController.php b/apps/backend/app/Http/Controllers/ChapterController.php new file mode 100644 index 0000000..e00747b --- /dev/null +++ b/apps/backend/app/Http/Controllers/ChapterController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateChapterRequest $request, string $id) + { + $item = Chapter::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Chapter::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/CriterionController.php b/apps/backend/app/Http/Controllers/CriterionController.php new file mode 100644 index 0000000..ec56f77 --- /dev/null +++ b/apps/backend/app/Http/Controllers/CriterionController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateCriterionRequest $request, string $id) + { + $item = Criterion::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Criterion::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/GlossaryController.php b/apps/backend/app/Http/Controllers/GlossaryController.php new file mode 100644 index 0000000..cf0c573 --- /dev/null +++ b/apps/backend/app/Http/Controllers/GlossaryController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateGlossaryRequest $request, string $id) + { + $item = Glossary::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Glossary::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/ImageController.php b/apps/backend/app/Http/Controllers/ImageController.php new file mode 100644 index 0000000..5e801b5 --- /dev/null +++ b/apps/backend/app/Http/Controllers/ImageController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateImageRequest $request, string $id) + { + $item = Image::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Image::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/PermissionController.php b/apps/backend/app/Http/Controllers/PermissionController.php new file mode 100644 index 0000000..3e84a84 --- /dev/null +++ b/apps/backend/app/Http/Controllers/PermissionController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdatePermissionRequest $request, string $id) + { + $item = Permission::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Permission::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/ProjectController.php b/apps/backend/app/Http/Controllers/ProjectController.php new file mode 100644 index 0000000..206647a --- /dev/null +++ b/apps/backend/app/Http/Controllers/ProjectController.php @@ -0,0 +1,41 @@ +validated()); + return response()->json($project, 201); + } + + public function update(UpdateProjectRequest $request, string $id) + { + $project = Project::findOrFail($id); + $project->update($request->validated()); + return response()->json($project, 200); + } + + public function delete(string $id) + { + Project::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/ProjectCrudController.php b/apps/backend/app/Http/Controllers/ProjectCrudController.php new file mode 100644 index 0000000..74bda9c --- /dev/null +++ b/apps/backend/app/Http/Controllers/ProjectCrudController.php @@ -0,0 +1,39 @@ +all()); + return response()->json($item, 201); + } + + public function update(UpdateProjectRequest $request, string $id) + { + $item = Project::findOrFail($id); + $item->update($request->all()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Project::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/RoleController.php b/apps/backend/app/Http/Controllers/RoleController.php new file mode 100644 index 0000000..7a5a22c --- /dev/null +++ b/apps/backend/app/Http/Controllers/RoleController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateRoleRequest $request, string $id) + { + $item = Role::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Role::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/SectionController.php b/apps/backend/app/Http/Controllers/SectionController.php new file mode 100644 index 0000000..54ff563 --- /dev/null +++ b/apps/backend/app/Http/Controllers/SectionController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateSectionRequest $request, string $id) + { + $item = Section::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Section::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/SubCriterionController.php b/apps/backend/app/Http/Controllers/SubCriterionController.php new file mode 100644 index 0000000..017c65c --- /dev/null +++ b/apps/backend/app/Http/Controllers/SubCriterionController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateSubCriterionRequest $request, string $id) + { + $item = SubCriterion::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + SubCriterion::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/TableController.php b/apps/backend/app/Http/Controllers/TableController.php new file mode 100644 index 0000000..b4edb89 --- /dev/null +++ b/apps/backend/app/Http/Controllers/TableController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateTableRequest $request, string $id) + { + $item = Table::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + Table::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/TextBlockController.php b/apps/backend/app/Http/Controllers/TextBlockController.php new file mode 100644 index 0000000..8320227 --- /dev/null +++ b/apps/backend/app/Http/Controllers/TextBlockController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateTextBlockRequest $request, string $id) + { + $item = TextBlock::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + TextBlock::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/TimeBlockController.php b/apps/backend/app/Http/Controllers/TimeBlockController.php new file mode 100644 index 0000000..b27a217 --- /dev/null +++ b/apps/backend/app/Http/Controllers/TimeBlockController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($item, 201); + } + + public function update(UpdateTimeBlockRequest $request, string $id) + { + $item = TimeBlock::findOrFail($id); + $item->update($request->validated()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + TimeBlock::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/UserController.php b/apps/backend/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..71e51d9 --- /dev/null +++ b/apps/backend/app/Http/Controllers/UserController.php @@ -0,0 +1,39 @@ +all()); + return response()->json($item, 201); + } + + public function update(UpdateUserRequest $request, string $id) + { + $item = User::findOrFail($id); + $item->update($request->all()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + User::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/UserMetaController.php b/apps/backend/app/Http/Controllers/UserMetaController.php new file mode 100644 index 0000000..81b90f7 --- /dev/null +++ b/apps/backend/app/Http/Controllers/UserMetaController.php @@ -0,0 +1,39 @@ +all()); + return response()->json($item, 201); + } + + public function update(UpdateUserMetaRequest $request, string $id) + { + $item = UserMeta::findOrFail($id); + $item->update($request->all()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + UserMeta::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/UserSubCriterionController.php b/apps/backend/app/Http/Controllers/UserSubCriterionController.php new file mode 100644 index 0000000..91721a9 --- /dev/null +++ b/apps/backend/app/Http/Controllers/UserSubCriterionController.php @@ -0,0 +1,39 @@ +all()); + return response()->json($item, 201); + } + + public function update(UpdateUserSubCriterionRequest $request, string $id) + { + $item = UserSubCriterion::findOrFail($id); + $item->update($request->all()); + return response()->json($item, 200); + } + + public function delete(string $id) + { + UserSubCriterion::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Controllers/YearController.php b/apps/backend/app/Http/Controllers/YearController.php new file mode 100644 index 0000000..c23c2f6 --- /dev/null +++ b/apps/backend/app/Http/Controllers/YearController.php @@ -0,0 +1,39 @@ +validated()); + return response()->json($year, 201); + } + + public function update(UpdateYearRequest $request, $id) + { + $year = Year::findOrFail($id); + $year->update($request->validated()); + return response()->json($year, 200); + } + + public function delete($id) + { + Year::findOrFail($id)->delete(); + return response()->json(null, 204); + } +} diff --git a/apps/backend/app/Http/Requests/AiRating/StoreAiRatingRequest.php b/apps/backend/app/Http/Requests/AiRating/StoreAiRatingRequest.php new file mode 100644 index 0000000..78f50d8 --- /dev/null +++ b/apps/backend/app/Http/Requests/AiRating/StoreAiRatingRequest.php @@ -0,0 +1,16 @@ + 'required|exists:sections,id', + 'rating' => 'required|integer|min:1|max:6', + 'rating_description' => 'nullable|string', + 'rating_checksum' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/AiRating/UpdateAiRatingRequest.php b/apps/backend/app/Http/Requests/AiRating/UpdateAiRatingRequest.php new file mode 100644 index 0000000..a8e435b --- /dev/null +++ b/apps/backend/app/Http/Requests/AiRating/UpdateAiRatingRequest.php @@ -0,0 +1,16 @@ + 'sometimes|exists:sections,id', + 'rating' => 'sometimes|integer|min:1|max:6', + 'rating_description' => 'nullable|string', + 'rating_checksum' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Chapter/StoreChapterRequest.php b/apps/backend/app/Http/Requests/Chapter/StoreChapterRequest.php new file mode 100644 index 0000000..dba5306 --- /dev/null +++ b/apps/backend/app/Http/Requests/Chapter/StoreChapterRequest.php @@ -0,0 +1,17 @@ + 'required|exists:projects,id', + 'position' => 'required|integer', + 'type' => 'required|string', + 'title' => 'required|string|max:255', + 'subtitle' => 'nullable|string|max:255', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Chapter/UpdateChapterRequest.php b/apps/backend/app/Http/Requests/Chapter/UpdateChapterRequest.php new file mode 100644 index 0000000..2d3d29c --- /dev/null +++ b/apps/backend/app/Http/Requests/Chapter/UpdateChapterRequest.php @@ -0,0 +1,17 @@ + 'sometimes|exists:projects,id', + 'position' => 'sometimes|integer', + 'type' => 'sometimes|string', + 'title' => 'sometimes|string|max:255', + 'subtitle' => 'nullable|string|max:255', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Criterion/StoreCriterionRequest.php b/apps/backend/app/Http/Requests/Criterion/StoreCriterionRequest.php new file mode 100644 index 0000000..8161884 --- /dev/null +++ b/apps/backend/app/Http/Requests/Criterion/StoreCriterionRequest.php @@ -0,0 +1,16 @@ + 'required|exists:years,id', + 'title' => 'required|string', + 'description' => 'nullable|string', + 'special_for_project_id' => 'nullable|exists:projects,id', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Criterion/UpdateCriterionRequest.php b/apps/backend/app/Http/Requests/Criterion/UpdateCriterionRequest.php new file mode 100644 index 0000000..2268564 --- /dev/null +++ b/apps/backend/app/Http/Requests/Criterion/UpdateCriterionRequest.php @@ -0,0 +1,16 @@ + 'sometimes|exists:years,id', + 'title' => 'sometimes|string', + 'description' => 'nullable|string', + 'special_for_project_id' => 'nullable|exists:projects,id', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Glossary/StoreGlossaryRequest.php b/apps/backend/app/Http/Requests/Glossary/StoreGlossaryRequest.php new file mode 100644 index 0000000..d64c249 --- /dev/null +++ b/apps/backend/app/Http/Requests/Glossary/StoreGlossaryRequest.php @@ -0,0 +1,15 @@ + 'required|exists:projects,id', + 'term' => 'required|string', + 'definition' => 'required|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Glossary/UpdateGlossaryRequest.php b/apps/backend/app/Http/Requests/Glossary/UpdateGlossaryRequest.php new file mode 100644 index 0000000..18f31ff --- /dev/null +++ b/apps/backend/app/Http/Requests/Glossary/UpdateGlossaryRequest.php @@ -0,0 +1,15 @@ + 'sometimes|exists:projects,id', + 'term' => 'sometimes|string', + 'definition' => 'sometimes|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Image/StoreImageRequest.php b/apps/backend/app/Http/Requests/Image/StoreImageRequest.php new file mode 100644 index 0000000..70b2040 --- /dev/null +++ b/apps/backend/app/Http/Requests/Image/StoreImageRequest.php @@ -0,0 +1,17 @@ + 'required|exists:sections,id', + 'position' => 'required|integer', + 'image_data' => 'required|string', + 'description' => 'nullable|string', + 'source' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Image/UpdateImageRequest.php b/apps/backend/app/Http/Requests/Image/UpdateImageRequest.php new file mode 100644 index 0000000..42ba2ba --- /dev/null +++ b/apps/backend/app/Http/Requests/Image/UpdateImageRequest.php @@ -0,0 +1,17 @@ + 'sometimes|exists:sections,id', + 'position' => 'sometimes|integer', + 'image_data' => 'sometimes|string', + 'description' => 'nullable|string', + 'source' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Permission/StorePermissionRequest.php b/apps/backend/app/Http/Requests/Permission/StorePermissionRequest.php new file mode 100644 index 0000000..6d2e0c0 --- /dev/null +++ b/apps/backend/app/Http/Requests/Permission/StorePermissionRequest.php @@ -0,0 +1,14 @@ + 'required|string|unique:permissions,name', + 'guard_name' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Permission/UpdatePermissionRequest.php b/apps/backend/app/Http/Requests/Permission/UpdatePermissionRequest.php new file mode 100644 index 0000000..bb54a07 --- /dev/null +++ b/apps/backend/app/Http/Requests/Permission/UpdatePermissionRequest.php @@ -0,0 +1,14 @@ + 'sometimes|string|unique:permissions,name,' . $this->route('id'), + 'guard_name' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Project/StoreProjectRequest.php b/apps/backend/app/Http/Requests/Project/StoreProjectRequest.php new file mode 100644 index 0000000..bdb7b4a --- /dev/null +++ b/apps/backend/app/Http/Requests/Project/StoreProjectRequest.php @@ -0,0 +1,25 @@ + 'required|exists:years,id', + 'owner_id' => 'required|exists:users,id', + 'name' => 'required|string|max:255', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'description' => 'nullable|string', + ]; + } +} diff --git a/apps/backend/app/Http/Requests/Project/UpdateProjectRequest.php b/apps/backend/app/Http/Requests/Project/UpdateProjectRequest.php new file mode 100644 index 0000000..6a2ccb5 --- /dev/null +++ b/apps/backend/app/Http/Requests/Project/UpdateProjectRequest.php @@ -0,0 +1,25 @@ + 'sometimes|exists:years,id', + 'owner_id' => 'sometimes|exists:users,id', + 'name' => 'sometimes|string|max:255', + 'start_date' => 'sometimes|date', + 'end_date' => 'sometimes|date|after_or_equal:start_date', + 'description' => 'nullable|string', + ]; + } +} diff --git a/apps/backend/app/Http/Requests/Role/StoreRoleRequest.php b/apps/backend/app/Http/Requests/Role/StoreRoleRequest.php new file mode 100644 index 0000000..144e627 --- /dev/null +++ b/apps/backend/app/Http/Requests/Role/StoreRoleRequest.php @@ -0,0 +1,14 @@ + 'required|string|unique:roles,name', + 'guard_name' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Role/UpdateRoleRequest.php b/apps/backend/app/Http/Requests/Role/UpdateRoleRequest.php new file mode 100644 index 0000000..b15d2c5 --- /dev/null +++ b/apps/backend/app/Http/Requests/Role/UpdateRoleRequest.php @@ -0,0 +1,14 @@ + 'sometimes|string|unique:roles,name,' . $this->route('id'), + 'guard_name' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Section/StoreSectionRequest.php b/apps/backend/app/Http/Requests/Section/StoreSectionRequest.php new file mode 100644 index 0000000..d9e72db --- /dev/null +++ b/apps/backend/app/Http/Requests/Section/StoreSectionRequest.php @@ -0,0 +1,18 @@ + 'required|exists:chapters,id', + 'parent_id' => 'nullable|exists:sections,id', + 'position' => 'required|integer', + 'title' => 'required|string', + 'subtitle' => 'nullable|string', + 'rating_checksum' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Section/UpdateSectionRequest.php b/apps/backend/app/Http/Requests/Section/UpdateSectionRequest.php new file mode 100644 index 0000000..a2f9e39 --- /dev/null +++ b/apps/backend/app/Http/Requests/Section/UpdateSectionRequest.php @@ -0,0 +1,18 @@ + 'sometimes|exists:chapters,id', + 'parent_id' => 'nullable|exists:sections,id', + 'position' => 'sometimes|integer', + 'title' => 'sometimes|string', + 'subtitle' => 'nullable|string', + 'rating_checksum' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/SubCriterion/StoreSubCriterionRequest.php b/apps/backend/app/Http/Requests/SubCriterion/StoreSubCriterionRequest.php new file mode 100644 index 0000000..ef6d756 --- /dev/null +++ b/apps/backend/app/Http/Requests/SubCriterion/StoreSubCriterionRequest.php @@ -0,0 +1,14 @@ + 'required|exists:criteria,id', + 'description' => 'required|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/SubCriterion/UpdateSubCriterionRequest.php b/apps/backend/app/Http/Requests/SubCriterion/UpdateSubCriterionRequest.php new file mode 100644 index 0000000..4fe5f57 --- /dev/null +++ b/apps/backend/app/Http/Requests/SubCriterion/UpdateSubCriterionRequest.php @@ -0,0 +1,14 @@ + 'sometimes|exists:criteria,id', + 'description' => 'sometimes|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Table/StoreTableRequest.php b/apps/backend/app/Http/Requests/Table/StoreTableRequest.php new file mode 100644 index 0000000..46242e4 --- /dev/null +++ b/apps/backend/app/Http/Requests/Table/StoreTableRequest.php @@ -0,0 +1,17 @@ + 'required|exists:sections,id', + 'position' => 'required|integer', + 'heading' => 'nullable|string', + 'markdown_table' => 'required|string', + 'source' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Table/UpdateTableRequest.php b/apps/backend/app/Http/Requests/Table/UpdateTableRequest.php new file mode 100644 index 0000000..a040450 --- /dev/null +++ b/apps/backend/app/Http/Requests/Table/UpdateTableRequest.php @@ -0,0 +1,17 @@ + 'sometimes|exists:sections,id', + 'position' => 'sometimes|integer', + 'heading' => 'nullable|string', + 'markdown_table' => 'sometimes|string', + 'source' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/TextBlock/StoreTextBlockRequest.php b/apps/backend/app/Http/Requests/TextBlock/StoreTextBlockRequest.php new file mode 100644 index 0000000..7f070ee --- /dev/null +++ b/apps/backend/app/Http/Requests/TextBlock/StoreTextBlockRequest.php @@ -0,0 +1,17 @@ + 'required|exists:sections,id', + 'position' => 'required|integer', + 'heading' => 'nullable|string', + 'text' => 'required|string', + 'source' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/TextBlock/UpdateTextBlockRequest.php b/apps/backend/app/Http/Requests/TextBlock/UpdateTextBlockRequest.php new file mode 100644 index 0000000..9a02d4f --- /dev/null +++ b/apps/backend/app/Http/Requests/TextBlock/UpdateTextBlockRequest.php @@ -0,0 +1,17 @@ + 'sometimes|exists:sections,id', + 'position' => 'sometimes|integer', + 'heading' => 'nullable|string', + 'text' => 'sometimes|string', + 'source' => 'nullable|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/TimeBlock/StoreTimeBlockRequest.php b/apps/backend/app/Http/Requests/TimeBlock/StoreTimeBlockRequest.php new file mode 100644 index 0000000..55c1aa0 --- /dev/null +++ b/apps/backend/app/Http/Requests/TimeBlock/StoreTimeBlockRequest.php @@ -0,0 +1,17 @@ + 'required|exists:projects,id', + 'description' => 'required|string', + 'is_chore' => 'required|boolean', + 'start_time' => 'required|date', + 'end_time' => 'required|date|after_or_equal:start_time', + ]; } +} diff --git a/apps/backend/app/Http/Requests/TimeBlock/UpdateTimeBlockRequest.php b/apps/backend/app/Http/Requests/TimeBlock/UpdateTimeBlockRequest.php new file mode 100644 index 0000000..41aff0e --- /dev/null +++ b/apps/backend/app/Http/Requests/TimeBlock/UpdateTimeBlockRequest.php @@ -0,0 +1,17 @@ + 'sometimes|exists:projects,id', + 'description' => 'sometimes|string', + 'is_chore' => 'sometimes|boolean', + 'start_time' => 'sometimes|date', + 'end_time' => 'sometimes|date|after_or_equal:start_time', + ]; } +} diff --git a/apps/backend/app/Http/Requests/User/StoreUserRequest.php b/apps/backend/app/Http/Requests/User/StoreUserRequest.php new file mode 100644 index 0000000..7a7bbcb --- /dev/null +++ b/apps/backend/app/Http/Requests/User/StoreUserRequest.php @@ -0,0 +1,16 @@ + 'required|string|max:255', + 'last_name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email', + 'password' => 'required|string|min:8', + ]; } +} diff --git a/apps/backend/app/Http/Requests/User/UpdateUserRequest.php b/apps/backend/app/Http/Requests/User/UpdateUserRequest.php new file mode 100644 index 0000000..3eab7d1 --- /dev/null +++ b/apps/backend/app/Http/Requests/User/UpdateUserRequest.php @@ -0,0 +1,16 @@ + 'sometimes|string|max:255', + 'last_name' => 'sometimes|string|max:255', + 'email' => 'sometimes|email|unique:users,email,' . $this->route('id'), + 'password' => 'sometimes|string|min:8', + ]; } +} diff --git a/apps/backend/app/Http/Requests/UserMeta/StoreUserMetaRequest.php b/apps/backend/app/Http/Requests/UserMeta/StoreUserMetaRequest.php new file mode 100644 index 0000000..09f6fcb --- /dev/null +++ b/apps/backend/app/Http/Requests/UserMeta/StoreUserMetaRequest.php @@ -0,0 +1,15 @@ + 'required|exists:users,id', + 'meta_key' => 'required|string', + 'meta_value' => 'required|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/UserMeta/UpdateUserMetaRequest.php b/apps/backend/app/Http/Requests/UserMeta/UpdateUserMetaRequest.php new file mode 100644 index 0000000..5d44066 --- /dev/null +++ b/apps/backend/app/Http/Requests/UserMeta/UpdateUserMetaRequest.php @@ -0,0 +1,15 @@ + 'sometimes|exists:users,id', + 'meta_key' => 'sometimes|string', + 'meta_value' => 'sometimes|string', + ]; } +} diff --git a/apps/backend/app/Http/Requests/UserSubCriterion/StoreUserSubCriterionRequest.php b/apps/backend/app/Http/Requests/UserSubCriterion/StoreUserSubCriterionRequest.php new file mode 100644 index 0000000..e4f5ce6 --- /dev/null +++ b/apps/backend/app/Http/Requests/UserSubCriterion/StoreUserSubCriterionRequest.php @@ -0,0 +1,15 @@ + 'required|exists:sub_criteria,id', + 'user_id' => 'required|exists:users,id', + 'is_fulfilled' => 'required|boolean', + ]; } +} diff --git a/apps/backend/app/Http/Requests/UserSubCriterion/UpdateUserSubCriterionRequest.php b/apps/backend/app/Http/Requests/UserSubCriterion/UpdateUserSubCriterionRequest.php new file mode 100644 index 0000000..4e31808 --- /dev/null +++ b/apps/backend/app/Http/Requests/UserSubCriterion/UpdateUserSubCriterionRequest.php @@ -0,0 +1,15 @@ + 'sometimes|exists:sub_criteria,id', + 'user_id' => 'sometimes|exists:users,id', + 'is_fulfilled' => 'sometimes|boolean', + ]; } +} diff --git a/apps/backend/app/Http/Requests/Year/StoreYearRequest.php b/apps/backend/app/Http/Requests/Year/StoreYearRequest.php new file mode 100644 index 0000000..ba1dbba --- /dev/null +++ b/apps/backend/app/Http/Requests/Year/StoreYearRequest.php @@ -0,0 +1,21 @@ + 'required|integer|min:1900|max:3000', + 'is_active' => 'required|boolean', + ]; + } +} diff --git a/apps/backend/app/Http/Requests/Year/UpdateYearRequest.php b/apps/backend/app/Http/Requests/Year/UpdateYearRequest.php new file mode 100644 index 0000000..fc85433 --- /dev/null +++ b/apps/backend/app/Http/Requests/Year/UpdateYearRequest.php @@ -0,0 +1,21 @@ + 'sometimes|integer|min:1900|max:3000', + 'is_active' => 'sometimes|boolean', + ]; + } +} diff --git a/apps/backend/app/Models/AiRating.php b/apps/backend/app/Models/AiRating.php index 8fee9ea..56090e8 100644 --- a/apps/backend/app/Models/AiRating.php +++ b/apps/backend/app/Models/AiRating.php @@ -2,11 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Str; class AiRating extends Model { + /** @use HasFactory<\Database\Factories\AiRatingFactory> */ + use HasFactory; + + protected $fillable = ['section_id', 'rating', 'rating_description', 'rating_checksum']; + protected $keyType = 'string'; public $incrementing = false; @@ -18,4 +25,11 @@ protected static function booted() } }); } + + // Relationships + + public function section(): BelongsTo + { + return $this->belongsTo(Section::class); + } } diff --git a/apps/backend/app/Models/Chapter.php b/apps/backend/app/Models/Chapter.php index 112054a..2c3c685 100644 --- a/apps/backend/app/Models/Chapter.php +++ b/apps/backend/app/Models/Chapter.php @@ -2,10 +2,19 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Str; class Chapter extends Model { + /** @use HasFactory<\Database\Factories\ChapterFactory> */ + use HasFactory; + + protected $fillable = ['project_id', 'type', 'title', 'subtitle']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +26,16 @@ protected static function booted() } }); } + + // Relationships + + public function project(): BelongsTo + { + return $this->belongsTo(Project::class); + } + + public function sections(): HasMany + { + return $this->hasMany(Section::class); + } } diff --git a/apps/backend/app/Models/Criterion.php b/apps/backend/app/Models/Criterion.php index 480535b..bd42546 100644 --- a/apps/backend/app/Models/Criterion.php +++ b/apps/backend/app/Models/Criterion.php @@ -2,10 +2,19 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Str; class Criterion extends Model { + /** @use HasFactory<\Database\Factories\CriterionFactory> */ + use HasFactory; + + protected $fillable = ['year_id', 'title', 'description', 'special_for_project_id']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +26,21 @@ protected static function booted() } }); } + + // Relationships + + public function year(): BelongsTo + { + return $this->belongsTo(Year::class); + } + + public function specialForProject(): BelongsTo + { + return $this->belongsTo(Project::class, 'special_for_project_id'); + } + + public function subCriteria(): HasMany + { + return $this->hasMany(SubCriterion::class, 'criteria_id'); + } } diff --git a/apps/backend/app/Models/Glossary.php b/apps/backend/app/Models/Glossary.php index a9acc92..1d44946 100644 --- a/apps/backend/app/Models/Glossary.php +++ b/apps/backend/app/Models/Glossary.php @@ -2,10 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; class Glossary extends Model { + /** @use HasFactory<\Database\Factories\GlossaryFactory> */ + use HasFactory; + + protected $fillable = ['project_id', 'term', 'definition']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +25,11 @@ protected static function booted() } }); } + + // Relationships + + public function project(): BelongsTo + { + return $this->belongsTo(Project::class); + } } diff --git a/apps/backend/app/Models/Image.php b/apps/backend/app/Models/Image.php index ff62cb5..6abcce0 100644 --- a/apps/backend/app/Models/Image.php +++ b/apps/backend/app/Models/Image.php @@ -2,10 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; class Image extends Model { + /** @use HasFactory<\Database\Factories\ImageFactory> */ + use HasFactory; + + protected $fillable = ['section_id', 'position', 'image_data', 'description', 'source']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +25,11 @@ protected static function booted() } }); } + + // Relationships + + public function section(): BelongsTo + { + return $this->belongsTo(Section::class); + } } diff --git a/apps/backend/app/Models/Permission.php b/apps/backend/app/Models/Permission.php new file mode 100644 index 0000000..a8ce061 --- /dev/null +++ b/apps/backend/app/Models/Permission.php @@ -0,0 +1,27 @@ + */ + use HasFactory; + + protected $keyType = 'string'; + public $incrementing = false; + + protected static function booted() + { + parent::booted(); + + static::creating(function (self $permission) { + if (empty($permission->id)) { + $permission->id = (string) Str::uuid(); + } + }); + } +} diff --git a/apps/backend/app/Models/Project.php b/apps/backend/app/Models/Project.php index d83884b..d56a4c2 100644 --- a/apps/backend/app/Models/Project.php +++ b/apps/backend/app/Models/Project.php @@ -2,10 +2,20 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Support\Str; class Project extends Model { + /** @use HasFactory<\Database\Factories\ProjectFactory> */ + use HasFactory; + + protected $fillable = ['year_id', 'name', 'owner_id', 'description', 'start_date', 'end_date']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +27,71 @@ protected static function booted() } }); } + + // Relationships + + /** + * The year this project belongs to. + */ + public function year(): BelongsTo + { + return $this->belongsTo(Year::class); + } + + /** + * The user who owns this project. + */ + public function owner(): BelongsTo + { + return $this->belongsTo(User::class, 'owner_id'); + } + + /** + * Chapters within this project. + */ + public function chapters(): HasMany + { + return $this->hasMany(Chapter::class); + } + + /** + * Sections within this project via chapters. + */ + public function sections(): HasManyThrough + { + return $this->hasManyThrough(Section::class, Chapter::class, 'project_id', 'chapter_id'); + } + + /** + * Glossary entries defined for this project. + */ + public function glossaryEntries(): HasMany + { + return $this->hasMany(Glossary::class); + } + + /** + * Time blocks associated with this project. + */ + public function timeBlocks(): HasMany + { + return $this->hasMany(TimeBlock::class); + } + + /** + * Special criteria tied specifically to this project. + */ + public function specialCriteria(): HasMany + { + return $this->hasMany(Criterion::class, 'special_for_project_id'); + } + + public function isOwnedByUser() + { + if ($this->owner_id == auth()->user()['id']) { + return true; + } else { + return false; + } + } } diff --git a/apps/backend/app/Models/Role.php b/apps/backend/app/Models/Role.php new file mode 100644 index 0000000..f0811af --- /dev/null +++ b/apps/backend/app/Models/Role.php @@ -0,0 +1,27 @@ + */ + use HasFactory; + + protected $keyType = 'string'; + public $incrementing = false; + + protected static function booted() + { + parent::booted(); + + static::creating(function (self $role) { + if (empty($role->id)) { + $role->id = (string) Str::uuid(); + } + }); + } +} diff --git a/apps/backend/app/Models/Section.php b/apps/backend/app/Models/Section.php index 7793d34..38b1967 100644 --- a/apps/backend/app/Models/Section.php +++ b/apps/backend/app/Models/Section.php @@ -2,10 +2,26 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Str; class Section extends Model { + /** @use HasFactory<\Database\Factories\SectionFactory> */ + use HasFactory; + + protected $fillable = [ + 'chapter_id', + 'parent_id', + 'position', + 'title', + 'subtitle', + 'rating_checksum', + ]; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +33,41 @@ protected static function booted() } }); } + + // Relationships + + public function chapter(): BelongsTo + { + return $this->belongsTo(Chapter::class); + } + + public function parent(): BelongsTo + { + return $this->belongsTo(Section::class, 'parent_id'); + } + + public function children(): HasMany + { + return $this->hasMany(Section::class, 'parent_id'); + } + + public function textBlocks(): HasMany + { + return $this->hasMany(TextBlock::class); + } + + public function images(): HasMany + { + return $this->hasMany(Image::class); + } + + public function tables(): HasMany + { + return $this->hasMany(Table::class); + } + + public function aiRatings(): HasMany + { + return $this->hasMany(AiRating::class); + } } diff --git a/apps/backend/app/Models/SubCriterion.php b/apps/backend/app/Models/SubCriterion.php index b88ae1d..a338ed8 100644 --- a/apps/backend/app/Models/SubCriterion.php +++ b/apps/backend/app/Models/SubCriterion.php @@ -2,10 +2,19 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Str; class SubCriterion extends Model { + /** @use HasFactory<\Database\Factories\SubCriterionFactory> */ + use HasFactory; + + protected $fillable = ['criteria_id', 'description']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +26,16 @@ protected static function booted() } }); } + + // Relationships + + public function criterion(): BelongsTo + { + return $this->belongsTo(Criterion::class, 'criteria_id'); + } + + public function userSubCriteria(): HasMany + { + return $this->hasMany(UserSubCriterion::class, 'sub_criteria_id'); + } } diff --git a/apps/backend/app/Models/Table.php b/apps/backend/app/Models/Table.php index 0e464ce..093fbbf 100644 --- a/apps/backend/app/Models/Table.php +++ b/apps/backend/app/Models/Table.php @@ -2,10 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; class Table extends Model { + /** @use HasFactory<\Database\Factories\TableFactory> */ + use HasFactory; + + protected $fillable = ['section_id', 'position', 'heading', 'markdown_table', 'source']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +25,11 @@ protected static function booted() } }); } + + // Relationships + + public function section(): BelongsTo + { + return $this->belongsTo(Section::class); + } } diff --git a/apps/backend/app/Models/TextBlock.php b/apps/backend/app/Models/TextBlock.php index 868da61..d7bf2b8 100644 --- a/apps/backend/app/Models/TextBlock.php +++ b/apps/backend/app/Models/TextBlock.php @@ -2,10 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; class TextBlock extends Model { + /** @use HasFactory<\Database\Factories\TextBlockFactory> */ + use HasFactory; + + protected $fillable = ['section_id', 'position', 'heading', 'text', 'source']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +25,11 @@ protected static function booted() } }); } + + // Relationships + + public function section(): BelongsTo + { + return $this->belongsTo(Section::class); + } } diff --git a/apps/backend/app/Models/TimeBlock.php b/apps/backend/app/Models/TimeBlock.php index 4cbd701..a3d8e74 100644 --- a/apps/backend/app/Models/TimeBlock.php +++ b/apps/backend/app/Models/TimeBlock.php @@ -2,10 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; class TimeBlock extends Model { + /** @use HasFactory<\Database\Factories\TimeBlockFactory> */ + use HasFactory; + + protected $fillable = ['project_id', 'description', 'is_chore', 'start_time', 'end_time']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +25,11 @@ protected static function booted() } }); } + + // Relationships + + public function project(): BelongsTo + { + return $this->belongsTo(Project::class); + } } diff --git a/apps/backend/app/Models/User.php b/apps/backend/app/Models/User.php index 6cce4d5..a4dc3e3 100644 --- a/apps/backend/app/Models/User.php +++ b/apps/backend/app/Models/User.php @@ -5,14 +5,17 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Str; +use Spatie\Permission\Traits\HasPermissions; +use Spatie\Permission\Traits\HasRoles; use Tymon\JWTAuth\Contracts\JWTSubject; class User extends Authenticatable implements JWTSubject { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, HasRoles, HasPermissions; protected $keyType = 'string'; public $incrementing = false; @@ -26,17 +29,9 @@ protected static function booted() }); } - protected $fillable = [ - 'first_name', - 'last_name', - 'email', - 'password', - ]; + protected $fillable = ['first_name', 'last_name', 'email', 'password']; - protected $hidden = [ - 'password', - 'remember_token', - ]; + protected $hidden = ['password', 'remember_token']; protected function casts() { @@ -55,4 +50,30 @@ public function getJWTCustomClaims() { return []; } + + // Relationships + + /** + * Projects owned by the user. + */ + public function projects(): HasMany + { + return $this->hasMany(Project::class, 'owner_id'); + } + + /** + * Additional metadata entries for the user. + */ + public function meta(): HasMany + { + return $this->hasMany(UserMeta::class); + } + + /** + * UserSubCriterion flags for this user. + */ + public function userSubCriteria(): HasMany + { + return $this->hasMany(UserSubCriterion::class); + } } diff --git a/apps/backend/app/Models/UserMeta.php b/apps/backend/app/Models/UserMeta.php index 57d7ccb..8b3d48a 100644 --- a/apps/backend/app/Models/UserMeta.php +++ b/apps/backend/app/Models/UserMeta.php @@ -2,10 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; class UserMeta extends Model { + /** @use HasFactory<\Database\Factories\UserMetaFactory> */ + use HasFactory; + + protected $fillable = ['user_id', 'meta_key', 'meta_value']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +25,11 @@ protected static function booted() } }); } + + // Relationships + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/apps/backend/app/Models/UserSubCriterion.php b/apps/backend/app/Models/UserSubCriterion.php index 668074f..7aef268 100644 --- a/apps/backend/app/Models/UserSubCriterion.php +++ b/apps/backend/app/Models/UserSubCriterion.php @@ -2,10 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; class UserSubCriterion extends Model { + /** @use HasFactory<\Database\Factories\UserSubCriterionFactory> */ + use HasFactory; + + protected $fillable = ['sub_criteria_id', 'user_id', 'is_fulfilled']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +25,16 @@ protected static function booted() } }); } + + // Relationships + + public function subCriterion(): BelongsTo + { + return $this->belongsTo(SubCriterion::class, 'sub_criteria_id'); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/apps/backend/app/Models/Year.php b/apps/backend/app/Models/Year.php index 52d6d72..3ffaa92 100644 --- a/apps/backend/app/Models/Year.php +++ b/apps/backend/app/Models/Year.php @@ -2,10 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Str; class Year extends Model { + /** @use HasFactory<\Database\Factories\YearFactory> */ + use HasFactory; + + protected $fillable = ['year', 'is_active']; + protected $keyType = 'string'; public $incrementing = false; @@ -17,4 +25,22 @@ protected static function booted() } }); } + + // Relationships + + /** + * Projects that belong to this year. + */ + public function projects(): HasMany + { + return $this->hasMany(Project::class); + } + + /** + * Criteria defined for this year. + */ + public function criteria(): HasMany + { + return $this->hasMany(Criterion::class); + } } diff --git a/apps/backend/bootstrap/app.php b/apps/backend/bootstrap/app.php index 8b8f233..8cfa27e 100644 --- a/apps/backend/bootstrap/app.php +++ b/apps/backend/bootstrap/app.php @@ -8,9 +8,9 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( - web: __DIR__.'/../routes/web.php', - api: __DIR__.'/../routes/api.php', - commands: __DIR__.'/../routes/console.php', + web: __DIR__ . '/../routes/web.php', + api: __DIR__ . '/../routes/api.php', + commands: __DIR__ . '/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { @@ -18,9 +18,11 @@ }) ->withExceptions(function (Exceptions $exceptions): void { // - }) ->withMiddleware(function (Middleware $middleware) { + }) + ->withMiddleware(function (Middleware $middleware) { // $middleware->alias([ - 'jwt' => JwtMiddleware::class + 'jwt' => JwtMiddleware::class, ]); - })->create(); + }) + ->create(); diff --git a/apps/backend/bootstrap/providers.php b/apps/backend/bootstrap/providers.php index 38b258d..2bd0b5c 100644 --- a/apps/backend/bootstrap/providers.php +++ b/apps/backend/bootstrap/providers.php @@ -1,5 +1,3 @@ env('APP_KEY'), - 'previous_keys' => [ - ...array_filter( - explode(',', (string) env('APP_PREVIOUS_KEYS', '')) - ), - ], + 'previous_keys' => [...array_filter(explode(',', (string) env('APP_PREVIOUS_KEYS', '')))], /* |-------------------------------------------------------------------------- @@ -122,5 +117,4 @@ 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], - ]; diff --git a/apps/backend/config/auth.php b/apps/backend/config/auth.php index 368c2f4..7cf9d71 100644 --- a/apps/backend/config/auth.php +++ b/apps/backend/config/auth.php @@ -1,7 +1,6 @@ env('AUTH_PASSWORD_TIMEOUT', 10800), - ]; diff --git a/apps/backend/config/cache.php b/apps/backend/config/cache.php index c2d927d..3fff2e2 100644 --- a/apps/backend/config/cache.php +++ b/apps/backend/config/cache.php @@ -3,7 +3,6 @@ use Illuminate\Support\Str; return [ - /* |-------------------------------------------------------------------------- | Default Cache Store @@ -32,7 +31,6 @@ */ 'stores' => [ - 'array' => [ 'driver' => 'array', 'serialize' => false, @@ -55,10 +53,7 @@ 'memcached' => [ 'driver' => 'memcached', 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), - 'sasl' => [ - env('MEMCACHED_USERNAME'), - env('MEMCACHED_PASSWORD'), - ], + 'sasl' => [env('MEMCACHED_USERNAME'), env('MEMCACHED_PASSWORD')], 'options' => [ // Memcached::OPT_CONNECT_TIMEOUT => 2000, ], @@ -89,7 +84,6 @@ 'octane' => [ 'driver' => 'octane', ], - ], /* @@ -103,6 +97,5 @@ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), - + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-cache-'), ]; diff --git a/apps/backend/config/database.php b/apps/backend/config/database.php index 53dcae0..6ba4961 100644 --- a/apps/backend/config/database.php +++ b/apps/backend/config/database.php @@ -3,7 +3,6 @@ use Illuminate\Support\Str; return [ - /* |-------------------------------------------------------------------------- | Default Database Connection Name @@ -30,7 +29,6 @@ */ 'connections' => [ - 'sqlite' => [ 'driver' => 'sqlite', 'url' => env('DB_URL'), @@ -58,9 +56,11 @@ 'prefix_indexes' => true, 'strict' => true, 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], + 'options' => extension_loaded('pdo_mysql') + ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) + : [], ], 'mariadb' => [ @@ -78,9 +78,11 @@ 'prefix_indexes' => true, 'strict' => true, 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], + 'options' => extension_loaded('pdo_mysql') + ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) + : [], ], 'pgsql' => [ @@ -112,7 +114,6 @@ // 'encrypt' => env('DB_ENCRYPT', 'yes'), // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), ], - ], /* @@ -143,12 +144,14 @@ */ 'redis' => [ - 'client' => env('REDIS_CLIENT', 'phpredis'), 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'prefix' => env( + 'REDIS_PREFIX', + Str::slug((string) env('APP_NAME', 'laravel')) . '-database-', + ), 'persistent' => env('REDIS_PERSISTENT', false), ], @@ -177,7 +180,5 @@ 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], - ], - ]; diff --git a/apps/backend/config/filesystems.php b/apps/backend/config/filesystems.php index 3d671bd..30e570e 100644 --- a/apps/backend/config/filesystems.php +++ b/apps/backend/config/filesystems.php @@ -1,7 +1,6 @@ [ - 'local' => [ 'driver' => 'local', 'root' => storage_path('app/private'), @@ -41,7 +39,7 @@ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'url' => env('APP_URL') . '/storage', 'visibility' => 'public', 'throw' => false, 'report' => false, @@ -59,7 +57,6 @@ 'throw' => false, 'report' => false, ], - ], /* @@ -76,5 +73,4 @@ 'links' => [ public_path('storage') => storage_path('app/public'), ], - ]; diff --git a/apps/backend/config/jwt.php b/apps/backend/config/jwt.php index f83234d..844d769 100644 --- a/apps/backend/config/jwt.php +++ b/apps/backend/config/jwt.php @@ -10,7 +10,6 @@ */ return [ - /* |-------------------------------------------------------------------------- | JWT Authentication Secret @@ -45,7 +44,6 @@ */ 'keys' => [ - /* |-------------------------------------------------------------------------- | Public Key @@ -82,7 +80,6 @@ */ 'passphrase' => env('JWT_PASSPHRASE'), - ], /* @@ -144,14 +141,7 @@ | */ - 'required_claims' => [ - 'iss', - 'iat', - 'exp', - 'nbf', - 'sub', - 'jti', - ], + 'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti'], /* |-------------------------------------------------------------------------- @@ -262,7 +252,6 @@ */ 'providers' => [ - /* |-------------------------------------------------------------------------- | JWT Provider @@ -295,7 +284,5 @@ */ 'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class, - ], - ]; diff --git a/apps/backend/config/logging.php b/apps/backend/config/logging.php index 9e998a4..6fbe654 100644 --- a/apps/backend/config/logging.php +++ b/apps/backend/config/logging.php @@ -6,7 +6,6 @@ use Monolog\Processor\PsrLogMessageProcessor; return [ - /* |-------------------------------------------------------------------------- | Default Log Channel @@ -51,7 +50,6 @@ */ 'channels' => [ - 'stack' => [ 'driver' => 'stack', 'channels' => explode(',', (string) env('LOG_STACK', 'single')), @@ -89,7 +87,8 @@ 'handler_with' => [ 'host' => env('PAPERTRAIL_URL'), 'port' => env('PAPERTRAIL_PORT'), - 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + 'connectionString' => + 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'), ], 'processors' => [PsrLogMessageProcessor::class], ], @@ -126,7 +125,5 @@ 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], - ], - ]; diff --git a/apps/backend/config/mail.php b/apps/backend/config/mail.php index 522b284..c22ccf6 100644 --- a/apps/backend/config/mail.php +++ b/apps/backend/config/mail.php @@ -1,7 +1,6 @@ [ - 'smtp' => [ 'transport' => 'smtp', 'scheme' => env('MAIL_SCHEME'), @@ -46,7 +44,10 @@ 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, - 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + 'local_domain' => env( + 'MAIL_EHLO_DOMAIN', + parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST), + ), ], 'ses' => [ @@ -81,22 +82,15 @@ 'failover' => [ 'transport' => 'failover', - 'mailers' => [ - 'smtp', - 'log', - ], + 'mailers' => ['smtp', 'log'], 'retry_after' => 60, ], 'roundrobin' => [ 'transport' => 'roundrobin', - 'mailers' => [ - 'ses', - 'postmark', - ], + 'mailers' => ['ses', 'postmark'], 'retry_after' => 60, ], - ], /* @@ -114,5 +108,4 @@ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 'name' => env('MAIL_FROM_NAME', 'Example'), ], - ]; diff --git a/apps/backend/config/permission.php b/apps/backend/config/permission.php new file mode 100644 index 0000000..add3d0a --- /dev/null +++ b/apps/backend/config/permission.php @@ -0,0 +1,197 @@ + [ + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => App\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => App\Models\Role::class, + ], + + 'table_names' => [ + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => 'role_id', // explicitly set for UUID support + 'permission_pivot_key' => 'permission_id', // explicitly set for UUID support + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttached + * \Spatie\Permission\Events\RoleDetached + * \Spatie\Permission\Events\PermissionAttached + * \Spatie\Permission\Events\PermissionDetached + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/apps/backend/config/queue.php b/apps/backend/config/queue.php index 116bd8d..ee2f2d0 100644 --- a/apps/backend/config/queue.php +++ b/apps/backend/config/queue.php @@ -1,7 +1,6 @@ [ - 'sync' => [ 'driver' => 'sync', ], @@ -71,7 +69,6 @@ 'block_for' => null, 'after_commit' => false, ], - ], /* @@ -108,5 +105,4 @@ 'database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'failed_jobs', ], - ]; diff --git a/apps/backend/config/sanctum.php b/apps/backend/config/sanctum.php index 44527d6..30b36d5 100644 --- a/apps/backend/config/sanctum.php +++ b/apps/backend/config/sanctum.php @@ -3,7 +3,6 @@ use Laravel\Sanctum\Sanctum; return [ - /* |-------------------------------------------------------------------------- | Stateful Domains @@ -15,12 +14,18 @@ | */ - 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( - '%s%s', - 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', - Sanctum::currentApplicationUrlWithPort(), - // Sanctum::currentRequestHost(), - ))), + 'stateful' => explode( + ',', + env( + 'SANCTUM_STATEFUL_DOMAINS', + sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ), + ), + ), /* |-------------------------------------------------------------------------- @@ -80,5 +85,4 @@ 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, ], - ]; diff --git a/apps/backend/config/services.php b/apps/backend/config/services.php index 6182e4b..2f2284f 100644 --- a/apps/backend/config/services.php +++ b/apps/backend/config/services.php @@ -1,7 +1,6 @@ env('SLACK_BOT_USER_DEFAULT_CHANNEL'), ], ], - ]; diff --git a/apps/backend/config/session.php b/apps/backend/config/session.php index bc45901..e7719ed 100644 --- a/apps/backend/config/session.php +++ b/apps/backend/config/session.php @@ -3,7 +3,6 @@ use Illuminate\Support\Str; return [ - /* |-------------------------------------------------------------------------- | Default Session Driver @@ -127,10 +126,7 @@ | */ - 'cookie' => env( - 'SESSION_COOKIE', - Str::slug((string) env('APP_NAME', 'laravel')).'-session' - ), + 'cookie' => env('SESSION_COOKIE', Str::slug((string) env('APP_NAME', 'laravel')) . '-session'), /* |-------------------------------------------------------------------------- @@ -213,5 +209,4 @@ */ 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), - ]; diff --git a/apps/backend/database/factories/AiRatingFactory.php b/apps/backend/database/factories/AiRatingFactory.php new file mode 100644 index 0000000..0b5abe5 --- /dev/null +++ b/apps/backend/database/factories/AiRatingFactory.php @@ -0,0 +1,35 @@ + + */ +class AiRatingFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = AiRating::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'section_id' => Section::factory(), + 'rating' => $this->faker->numberBetween(1, 10), + 'rating_description' => $this->faker->paragraph(), + 'rating_checksum' => $this->faker->sha256(), + ]; + } +} diff --git a/apps/backend/database/factories/ChapterFactory.php b/apps/backend/database/factories/ChapterFactory.php new file mode 100644 index 0000000..550c7b0 --- /dev/null +++ b/apps/backend/database/factories/ChapterFactory.php @@ -0,0 +1,35 @@ + + */ +class ChapterFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Chapter::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'project_id' => Project::factory(), + 'type' => $this->faker->randomElement(['introduction', 'main', 'conclusion']), + 'title' => $this->faker->sentence(3), + 'subtitle' => $this->faker->sentence(5), + ]; + } +} diff --git a/apps/backend/database/factories/CriterionFactory.php b/apps/backend/database/factories/CriterionFactory.php new file mode 100644 index 0000000..58f72f8 --- /dev/null +++ b/apps/backend/database/factories/CriterionFactory.php @@ -0,0 +1,34 @@ + + */ +class CriterionFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Criterion::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'year_id' => \App\Models\Year::factory(), + 'title' => $this->faker->sentence(3), + 'description' => $this->faker->paragraph(), + 'special_for_project_id' => null, + ]; + } +} diff --git a/apps/backend/database/factories/GlossaryFactory.php b/apps/backend/database/factories/GlossaryFactory.php new file mode 100644 index 0000000..b82ede2 --- /dev/null +++ b/apps/backend/database/factories/GlossaryFactory.php @@ -0,0 +1,34 @@ + + */ +class GlossaryFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Glossary::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'project_id' => Project::factory(), + 'term' => $this->faker->word(), + 'definition' => $this->faker->paragraph(), + ]; + } +} diff --git a/apps/backend/database/factories/ImageFactory.php b/apps/backend/database/factories/ImageFactory.php new file mode 100644 index 0000000..01d477a --- /dev/null +++ b/apps/backend/database/factories/ImageFactory.php @@ -0,0 +1,36 @@ + + */ +class ImageFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Image::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'section_id' => Section::factory(), + 'position' => $this->faker->numberBetween(1, 10), + 'image_data' => $this->faker->imageUrl(640, 480), + 'description' => $this->faker->sentence(), + 'source' => $this->faker->url(), + ]; + } +} diff --git a/apps/backend/database/factories/PermissionFactory.php b/apps/backend/database/factories/PermissionFactory.php new file mode 100644 index 0000000..db369d0 --- /dev/null +++ b/apps/backend/database/factories/PermissionFactory.php @@ -0,0 +1,32 @@ + + */ +class PermissionFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Permission::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->unique()->word(), + 'guard_name' => 'web', + ]; + } +} diff --git a/apps/backend/database/factories/ProjectFactory.php b/apps/backend/database/factories/ProjectFactory.php new file mode 100644 index 0000000..29f8f30 --- /dev/null +++ b/apps/backend/database/factories/ProjectFactory.php @@ -0,0 +1,38 @@ + + */ +class ProjectFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Project::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'year_id' => Year::factory(), + 'name' => $this->faker->sentence(3), + 'owner_id' => User::factory(), + 'description' => $this->faker->paragraph(), + 'start_date' => $this->faker->date(), + 'end_date' => $this->faker->date(), + ]; + } +} diff --git a/apps/backend/database/factories/RoleFactory.php b/apps/backend/database/factories/RoleFactory.php new file mode 100644 index 0000000..0d7e177 --- /dev/null +++ b/apps/backend/database/factories/RoleFactory.php @@ -0,0 +1,32 @@ + + */ +class RoleFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Role::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->unique()->word(), + 'guard_name' => 'web', + ]; + } +} diff --git a/apps/backend/database/factories/SectionFactory.php b/apps/backend/database/factories/SectionFactory.php new file mode 100644 index 0000000..3d290e8 --- /dev/null +++ b/apps/backend/database/factories/SectionFactory.php @@ -0,0 +1,37 @@ + + */ +class SectionFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Section::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'chapter_id' => Chapter::factory(), + 'parent_id' => null, + 'position' => $this->faker->numberBetween(1, 10), + 'title' => $this->faker->sentence(3), + 'subtitle' => $this->faker->sentence(5), + 'rating_checksum' => $this->faker->sha256(), + ]; + } +} diff --git a/apps/backend/database/factories/SubCriterionFactory.php b/apps/backend/database/factories/SubCriterionFactory.php new file mode 100644 index 0000000..58822a0 --- /dev/null +++ b/apps/backend/database/factories/SubCriterionFactory.php @@ -0,0 +1,33 @@ + + */ +class SubCriterionFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = SubCriterion::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'criteria_id' => Criterion::factory(), + 'description' => $this->faker->paragraph(), + ]; + } +} diff --git a/apps/backend/database/factories/TableFactory.php b/apps/backend/database/factories/TableFactory.php new file mode 100644 index 0000000..3ce0404 --- /dev/null +++ b/apps/backend/database/factories/TableFactory.php @@ -0,0 +1,37 @@ + + */ +class TableFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Table::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'section_id' => Section::factory(), + 'position' => $this->faker->numberBetween(1, 10), + 'heading' => $this->faker->sentence(3), + 'markdown_table' => + '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |', + 'source' => $this->faker->url(), + ]; + } +} diff --git a/apps/backend/database/factories/TextBlockFactory.php b/apps/backend/database/factories/TextBlockFactory.php new file mode 100644 index 0000000..3b8ebc9 --- /dev/null +++ b/apps/backend/database/factories/TextBlockFactory.php @@ -0,0 +1,36 @@ + + */ +class TextBlockFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = TextBlock::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'section_id' => Section::factory(), + 'position' => $this->faker->numberBetween(1, 10), + 'heading' => $this->faker->sentence(3), + 'text' => $this->faker->paragraphs(3, true), + 'source' => $this->faker->url(), + ]; + } +} diff --git a/apps/backend/database/factories/TimeBlockFactory.php b/apps/backend/database/factories/TimeBlockFactory.php new file mode 100644 index 0000000..97ac8ab --- /dev/null +++ b/apps/backend/database/factories/TimeBlockFactory.php @@ -0,0 +1,39 @@ + + */ +class TimeBlockFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = TimeBlock::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $startTime = $this->faker->dateTimeThisYear(); + $endTime = (clone $startTime)->modify('+' . $this->faker->numberBetween(1, 8) . ' hours'); + + return [ + 'project_id' => Project::factory(), + 'description' => $this->faker->sentence(), + 'is_chore' => $this->faker->boolean(), + 'start_time' => $startTime, + 'end_time' => $endTime, + ]; + } +} diff --git a/apps/backend/database/factories/UserFactory.php b/apps/backend/database/factories/UserFactory.php index 584104c..56ab8e8 100644 --- a/apps/backend/database/factories/UserFactory.php +++ b/apps/backend/database/factories/UserFactory.php @@ -24,21 +24,11 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), + 'first_name' => fake()->firstName(), + 'last_name' => fake()->lastName(), 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), + 'password' => (static::$password ??= Hash::make('password')), 'remember_token' => Str::random(10), ]; } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } } diff --git a/apps/backend/database/factories/UserMetaFactory.php b/apps/backend/database/factories/UserMetaFactory.php new file mode 100644 index 0000000..f527703 --- /dev/null +++ b/apps/backend/database/factories/UserMetaFactory.php @@ -0,0 +1,34 @@ + + */ +class UserMetaFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = UserMeta::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'meta_key' => $this->faker->word(), + 'meta_value' => $this->faker->sentence(), + ]; + } +} diff --git a/apps/backend/database/factories/UserSubCriterionFactory.php b/apps/backend/database/factories/UserSubCriterionFactory.php new file mode 100644 index 0000000..95118a1 --- /dev/null +++ b/apps/backend/database/factories/UserSubCriterionFactory.php @@ -0,0 +1,35 @@ + + */ +class UserSubCriterionFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = UserSubCriterion::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'sub_criteria_id' => SubCriterion::factory(), + 'user_id' => User::factory(), + 'is_fulfilled' => $this->faker->boolean(), + ]; + } +} diff --git a/apps/backend/database/factories/YearFactory.php b/apps/backend/database/factories/YearFactory.php new file mode 100644 index 0000000..5ee596b --- /dev/null +++ b/apps/backend/database/factories/YearFactory.php @@ -0,0 +1,32 @@ + + */ +class YearFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Year::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'year' => $this->faker->numberBetween(2020, 2030), + 'is_active' => $this->faker->boolean(), + ]; + } +} diff --git a/apps/backend/database/migrations/0001_01_01_000000_create_users_table.php b/apps/backend/database/migrations/0001_01_01_000000_create_users_table.php index de2c40f..47689cc 100644 --- a/apps/backend/database/migrations/0001_01_01_000000_create_users_table.php +++ b/apps/backend/database/migrations/0001_01_01_000000_create_users_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/0001_01_01_000001_create_cache_table.php b/apps/backend/database/migrations/0001_01_01_000001_create_cache_table.php index b9c106b..1d3e5b4 100644 --- a/apps/backend/database/migrations/0001_01_01_000001_create_cache_table.php +++ b/apps/backend/database/migrations/0001_01_01_000001_create_cache_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/0001_01_01_000002_create_jobs_table.php b/apps/backend/database/migrations/0001_01_01_000002_create_jobs_table.php index 425e705..f3e3e2d 100644 --- a/apps/backend/database/migrations/0001_01_01_000002_create_jobs_table.php +++ b/apps/backend/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/01_2025_10_03_225024_create_years_table.php b/apps/backend/database/migrations/01_2025_10_03_225024_create_years_table.php index 72e399e..a6af485 100644 --- a/apps/backend/database/migrations/01_2025_10_03_225024_create_years_table.php +++ b/apps/backend/database/migrations/01_2025_10_03_225024_create_years_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/02_2025_10_03_225024_create_projects_table.php b/apps/backend/database/migrations/02_2025_10_03_225024_create_projects_table.php index 0e5343b..3fc5480 100644 --- a/apps/backend/database/migrations/02_2025_10_03_225024_create_projects_table.php +++ b/apps/backend/database/migrations/02_2025_10_03_225024_create_projects_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/03_2025_10_03_225025_create_chapters_table.php b/apps/backend/database/migrations/03_2025_10_03_225025_create_chapters_table.php index 6f9c764..ad6cd2d 100644 --- a/apps/backend/database/migrations/03_2025_10_03_225025_create_chapters_table.php +++ b/apps/backend/database/migrations/03_2025_10_03_225025_create_chapters_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ @@ -14,6 +13,7 @@ public function up(): void Schema::create('chapters', function (Blueprint $table) { $table->char('id', 36)->primary(); $table->char('project_id', 36); + $table->integer('position'); $table->string('type'); $table->string('title'); $table->text('subtitle'); diff --git a/apps/backend/database/migrations/04_2025_10_03_225025_create_sections_table.php b/apps/backend/database/migrations/04_2025_10_03_225025_create_sections_table.php index 6a82355..c63ddf0 100644 --- a/apps/backend/database/migrations/04_2025_10_03_225025_create_sections_table.php +++ b/apps/backend/database/migrations/04_2025_10_03_225025_create_sections_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_221436_create_personal_access_tokens_table.php b/apps/backend/database/migrations/2025_10_03_221436_create_personal_access_tokens_table.php index 40ff706..4279a2e 100644 --- a/apps/backend/database/migrations/2025_10_03_221436_create_personal_access_tokens_table.php +++ b/apps/backend/database/migrations/2025_10_03_221436_create_personal_access_tokens_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_225024_create_criteria_table.php b/apps/backend/database/migrations/2025_10_03_225024_create_criteria_table.php index 673cea4..ae1a02d 100644 --- a/apps/backend/database/migrations/2025_10_03_225024_create_criteria_table.php +++ b/apps/backend/database/migrations/2025_10_03_225024_create_criteria_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_225024_create_glossary_table.php b/apps/backend/database/migrations/2025_10_03_225024_create_glossaries_table.php similarity index 80% rename from apps/backend/database/migrations/2025_10_03_225024_create_glossary_table.php rename to apps/backend/database/migrations/2025_10_03_225024_create_glossaries_table.php index f7c83d2..880709d 100644 --- a/apps/backend/database/migrations/2025_10_03_225024_create_glossary_table.php +++ b/apps/backend/database/migrations/2025_10_03_225024_create_glossaries_table.php @@ -4,14 +4,13 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ public function up(): void { - Schema::create('glossary', function (Blueprint $table) { + Schema::create('glossaries', function (Blueprint $table) { $table->char('id', 36)->primary(); $table->char('project_id', 36); $table->string('term'); @@ -27,6 +26,6 @@ public function up(): void */ public function down(): void { - Schema::dropIfExists('glossary'); + Schema::dropIfExists('glossaries'); } }; diff --git a/apps/backend/database/migrations/2025_10_03_225024_create_sub_criteria_table.php b/apps/backend/database/migrations/2025_10_03_225024_create_sub_criteria_table.php index 00b9964..4d7cb99 100644 --- a/apps/backend/database/migrations/2025_10_03_225024_create_sub_criteria_table.php +++ b/apps/backend/database/migrations/2025_10_03_225024_create_sub_criteria_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_225024_create_time_blocks_table.php b/apps/backend/database/migrations/2025_10_03_225024_create_time_blocks_table.php index bfe3397..290ed32 100644 --- a/apps/backend/database/migrations/2025_10_03_225024_create_time_blocks_table.php +++ b/apps/backend/database/migrations/2025_10_03_225024_create_time_blocks_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_225024_create_user_meta_table.php b/apps/backend/database/migrations/2025_10_03_225024_create_user_metas_table.php similarity index 80% rename from apps/backend/database/migrations/2025_10_03_225024_create_user_meta_table.php rename to apps/backend/database/migrations/2025_10_03_225024_create_user_metas_table.php index 437cd2c..13d76bb 100644 --- a/apps/backend/database/migrations/2025_10_03_225024_create_user_meta_table.php +++ b/apps/backend/database/migrations/2025_10_03_225024_create_user_metas_table.php @@ -4,14 +4,13 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ public function up(): void { - Schema::create('user_meta', function (Blueprint $table) { + Schema::create('user_metas', function (Blueprint $table) { $table->char('id', 36)->primary(); $table->char('user_id', 36); $table->string('meta_key'); @@ -27,6 +26,6 @@ public function up(): void */ public function down(): void { - Schema::dropIfExists('user_meta'); + Schema::dropIfExists('user_metas'); } }; diff --git a/apps/backend/database/migrations/2025_10_03_225024_create_user_sub_criteria_table.php b/apps/backend/database/migrations/2025_10_03_225024_create_user_sub_criteria_table.php index 5b84841..f346b54 100644 --- a/apps/backend/database/migrations/2025_10_03_225024_create_user_sub_criteria_table.php +++ b/apps/backend/database/migrations/2025_10_03_225024_create_user_sub_criteria_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_225025_create_ai_ratings_table.php b/apps/backend/database/migrations/2025_10_03_225025_create_ai_ratings_table.php index a5e38e3..71e0c05 100644 --- a/apps/backend/database/migrations/2025_10_03_225025_create_ai_ratings_table.php +++ b/apps/backend/database/migrations/2025_10_03_225025_create_ai_ratings_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_225025_create_images_table.php b/apps/backend/database/migrations/2025_10_03_225025_create_images_table.php index cee7ac1..816e117 100644 --- a/apps/backend/database/migrations/2025_10_03_225025_create_images_table.php +++ b/apps/backend/database/migrations/2025_10_03_225025_create_images_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_225025_create_tables_table.php b/apps/backend/database/migrations/2025_10_03_225025_create_tables_table.php index 1a8a1c8..7d50104 100644 --- a/apps/backend/database/migrations/2025_10_03_225025_create_tables_table.php +++ b/apps/backend/database/migrations/2025_10_03_225025_create_tables_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_03_225025_create_text_blocks_table.php b/apps/backend/database/migrations/2025_10_03_225025_create_text_blocks_table.php index 6a6dd58..ef61c21 100644 --- a/apps/backend/database/migrations/2025_10_03_225025_create_text_blocks_table.php +++ b/apps/backend/database/migrations/2025_10_03_225025_create_text_blocks_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/apps/backend/database/migrations/2025_10_04_014456_create_permission_tables.php b/apps/backend/database/migrations/2025_10_04_014456_create_permission_tables.php new file mode 100644 index 0000000..2970488 --- /dev/null +++ b/apps/backend/database/migrations/2025_10_04_014456_create_permission_tables.php @@ -0,0 +1,202 @@ +engine('InnoDB'); + $table->uuid('id')->primary()->default(new Expression('(UUID())')); + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], static function (Blueprint $table) use ( + $teams, + $columnNames, + ) { + // $table->engine('InnoDB'); + $table->uuid('id')->primary()->default(new Expression('(UUID())')); + if ($teams || config('permission.testing')) { + // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function ( + Blueprint $table, + ) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->uuid($pivotPermission); + $table->string('model_type'); + $table->uuid($columnNames['model_morph_key']); + $table->index( + [$columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_model_id_model_type_index', + ); + + $table + ->foreign($pivotPermission) + ->references('id') + ->on($tableNames['permissions']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index( + $columnNames['team_foreign_key'], + 'model_has_permissions_team_foreign_key_index', + ); + + $table->primary( + [ + $columnNames['team_foreign_key'], + $pivotPermission, + $columnNames['model_morph_key'], + 'model_type', + ], + 'model_has_permissions_permission_model_type_primary', + ); + } else { + $table->primary( + [$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary', + ); + } + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ( + $tableNames, + $columnNames, + $pivotRole, + $teams, + ) { + $table->uuid($pivotRole); + + $table->string('model_type'); + $table->uuid($columnNames['model_morph_key']); + $table->index( + [$columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_model_id_model_type_index', + ); + + $table + ->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index( + $columnNames['team_foreign_key'], + 'model_has_roles_team_foreign_key_index', + ); + + $table->primary( + [ + $columnNames['team_foreign_key'], + $pivotRole, + $columnNames['model_morph_key'], + 'model_type', + ], + 'model_has_roles_role_model_type_primary', + ); + } else { + $table->primary( + [$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary', + ); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ( + $tableNames, + $pivotRole, + $pivotPermission, + ) { + $table->uuid($pivotPermission); + $table->uuid($pivotRole); + + $table + ->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table + ->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary( + [$pivotPermission, $pivotRole], + 'role_has_permissions_permission_id_role_id_primary', + ); + }); + + app('cache') + ->store( + config('permission.cache.store') != 'default' + ? config('permission.cache.store') + : null, + ) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + if (empty($tableNames)) { + throw new \Exception( + 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.', + ); + } + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +}; diff --git a/apps/backend/database/migrations/2025_10_04_021216_create_roles_and_permissions.php b/apps/backend/database/migrations/2025_10_04_021216_create_roles_and_permissions.php new file mode 100644 index 0000000..0d1046b --- /dev/null +++ b/apps/backend/database/migrations/2025_10_04_021216_create_roles_and_permissions.php @@ -0,0 +1,52 @@ + 'apprentice']); + $mentor_role = \App\Models\Role::create(['name' => 'mentor']); + $supervisor_role = \App\Models\Role::create(['name' => 'supervisor']); + $admin_role = \App\Models\Role::create(['name' => 'admin']); + + $view_projects = \App\Models\Permission::create(['name' => 'view projects']); + $edit_projects = \App\Models\Permission::create(['name' => 'edit projects']); + $create_projects = \App\Models\Permission::create(['name' => 'create projects']); + $manage_projects = \App\Models\Permission::create(['name' => 'manage projects']); + + $view_users = \App\Models\Permission::create(['name' => 'view users']); + $manage_users = \App\Models\Permission::create(['name' => 'manage users']); + + $view_years = \App\Models\Permission::create(['name' => 'view years']); + $manage_years = \App\Models\Permission::create(['name' => 'manage years']); + + $view_criteria = \App\Models\Permission::create(['name' => 'view criteria']); + $manage_criteria = \App\Models\Permission::create(['name' => 'manage criteria']); + + $apprentice_role->givePermissionTo($edit_projects, $view_criteria); + $mentor_role->givePermissionTo( + $create_projects, + $view_projects, + $view_users, + $view_years, + $view_criteria, + $manage_criteria, + ); + $supervisor_role->givePermissionTo( + $manage_projects, + $view_users, + $manage_years, + $manage_criteria, + ); + $admin_role->givePermissionTo(\App\Models\Permission::all()); + } + + /** + * Reverse the migrations. + */ + public function down(): void {} +}; diff --git a/apps/backend/database/seeders/DatabaseSeeder.php b/apps/backend/database/seeders/DatabaseSeeder.php index d01a0ef..556f89b 100644 --- a/apps/backend/database/seeders/DatabaseSeeder.php +++ b/apps/backend/database/seeders/DatabaseSeeder.php @@ -2,22 +2,191 @@ namespace Database\Seeders; -use App\Models\User; -// use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use App\Models\{ + Year, + User, + Project, + Chapter, + Section, + TextBlock, + Table as TableModel, + Image, + AiRating, + Glossary, + Criterion, + SubCriterion, + TimeBlock, + UserMeta, + UserSubCriterion, +}; + class DatabaseSeeder extends Seeder { /** - * Seed the application's database. + * Seed the application's database with simple demo data. */ public function run(): void { - // User::factory(10)->create(); + // Base data + $years = Year::factory()->count(rand(5, 10))->create(); + $users = User::factory()->count(rand(5, 10))->create(); + + $admin = User::factory()->create([ + 'first_name' => 'Herr', + 'last_name' => 'Admin', + 'email' => 'admin@sesh.com', + 'password' => bcrypt('password'), + ]); + + $apprentice = User::factory()->create([ + 'first_name' => 'Herr', + 'last_name' => 'Lehrling', + 'email' => 'apprentice@sesh.com', + 'password' => bcrypt('password'), + ]); + + $supervisor = User::factory()->create([ + 'first_name' => 'Herr', + 'last_name' => 'Berufsausbilder', + 'email' => 'supervisor@sesh.com', + 'password' => bcrypt('password'), + ]); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $mentor = User::factory()->create([ + 'first_name' => 'Herr', + 'last_name' => 'Praxisbildner', + 'email' => 'mentor@sesh.com', + 'password' => bcrypt('password'), ]); + + $adminUser = User::where('email', 'admin@sesh.com')->first(); + $adminUser->assignRole('admin'); + + $apprenticeUser = User::where('email', 'apprentice@sesh.com')->first(); + $apprenticeUser->assignRole('apprentice'); + + $supervisorUser = User::where('email', 'supervisor@sesh.com')->first(); + $supervisorUser->assignRole('supervisor'); + + $mentorUser = User::where('email', 'mentor@sesh.com')->first(); + $mentorUser->assignRole('mentor'); + + // Projects linked to existing Years and Users + $projects = Project::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (Project $project) use ($years, $users) { + $project->year_id = $years->random()->id; + $project->owner_id = $users->random()->id; + $project->save(); + }); + + // Chapters linked to Projects + $chapters = Chapter::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (Chapter $chapter) use ($projects) { + $chapter->project_id = $projects->random()->id; + $chapter->save(); + }); + + // Sections linked to Chapters + $sections = Section::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (Section $section) use ($chapters) { + $section->chapter_id = $chapters->random()->id; + $section->parent_id = null; + $section->save(); + }); + + // Content linked to Sections + TextBlock::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (TextBlock $tb) use ($sections) { + $tb->section_id = $sections->random()->id; + $tb->save(); + }); + + TableModel::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (TableModel $tbl) use ($sections) { + $tbl->section_id = $sections->random()->id; + $tbl->save(); + }); + + Image::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (Image $img) use ($sections) { + $img->section_id = $sections->random()->id; + $img->save(); + }); + + AiRating::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (AiRating $r) use ($sections) { + $r->section_id = $sections->random()->id; + $r->save(); + }); + + // Glossary terms linked to Projects + Glossary::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (Glossary $g) use ($projects) { + $g->project_id = $projects->random()->id; + $g->save(); + }); + + // Criteria and SubCriteria linked to Years/Criteria + $criteria = Criterion::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (Criterion $c) use ($years) { + $c->year_id = $years->random()->id; + $c->special_for_project_id = null; + $c->save(); + }); + + $subCriteria = SubCriterion::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (SubCriterion $sc) use ($criteria) { + $sc->criteria_id = $criteria->random()->id; + $sc->save(); + }); + + // Time tracking linked to Projects + TimeBlock::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (TimeBlock $tb) use ($projects) { + $tb->project_id = $projects->random()->id; + $tb->save(); + }); + + // User meta and user-sub-criteria + UserMeta::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (UserMeta $um) use ($users) { + $um->user_id = $users->random()->id; + $um->save(); + }); + + UserSubCriterion::factory() + ->count(rand(5, 10)) + ->make() + ->each(function (UserSubCriterion $usc) use ($users, $subCriteria) { + $usc->user_id = $users->random()->id; + $usc->sub_criteria_id = $subCriteria->random()->id; + $usc->save(); + }); } } diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index 24a35c4..66a3b3e 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -5,10 +5,12 @@ "packages": { "": { "devDependencies": { + "@prettier/plugin-php": "^0.24.0", "@tailwindcss/vite": "^4.0.0", "axios": "^1.11.0", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0.0", + "prettier": "^3.6.2", "tailwindcss": "^4.0.0", "vite": "^7.0.7" } @@ -518,6 +520,20 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@prettier/plugin-php": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.24.0.tgz", + "integrity": "sha512-x9l65fCE/pgoET6RQowgdgG8Xmzs44z6j6Hhg3coINCyCw9JBGJ5ZzMR2XHAM2jmAdbJAIgqB6cUn4/3W3XLTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "linguist-languages": "^8.0.0", + "php-parser": "^3.2.5" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", @@ -1920,6 +1936,13 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linguist-languages": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/linguist-languages/-/linguist-languages-8.2.0.tgz", + "integrity": "sha512-KCUUH9x97QWYU0SXOCGxUrZR6cSfuQrMhABB7L/0I8N0LXOeaKe7+RZs7FAwvWCV2qKfZ4Wv1luLq4OfMezSJg==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -2005,6 +2028,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/php-parser": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.2.5.tgz", + "integrity": "sha512-M1ZYlALFFnESbSdmRtTQrBFUHSriHgPhgqtTF/LCbZM4h7swR5PHtUceB2Kzby5CfqcsYwBn7OXTJ0+8Sajwkw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2054,6 +2084,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", diff --git a/apps/backend/package.json b/apps/backend/package.json index af0db45..0cea0b2 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -7,10 +7,12 @@ "dev": "vite" }, "devDependencies": { + "@prettier/plugin-php": "^0.24.0", "@tailwindcss/vite": "^4.0.0", "axios": "^1.11.0", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0.0", + "prettier": "^3.6.2", "tailwindcss": "^4.0.0", "vite": "^7.0.7" } diff --git a/apps/backend/public/index.php b/apps/backend/public/index.php index ee8f07e..86bfe78 100644 --- a/apps/backend/public/index.php +++ b/apps/backend/public/index.php @@ -6,15 +6,15 @@ define('LARAVEL_START', microtime(true)); // Determine if the application is in maintenance mode... -if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) { +if (file_exists($maintenance = __DIR__ . '/../storage/framework/maintenance.php')) { require $maintenance; } // Register the Composer autoloader... -require __DIR__.'/../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; // Bootstrap Laravel and handle the request... /** @var Application $app */ -$app = require_once __DIR__.'/../bootstrap/app.php'; +$app = require_once __DIR__ . '/../bootstrap/app.php'; $app->handleRequest(Request::capture()); diff --git a/apps/backend/routes/api.php b/apps/backend/routes/api.php index 2b830a8..7bd8374 100644 --- a/apps/backend/routes/api.php +++ b/apps/backend/routes/api.php @@ -3,15 +3,71 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use App\Http\Controllers\AuthController; -use App\Http\Controllers\UserController; +use App\Http\Controllers\AppController; +$tables = [ + 'ai_ratings', + 'cache', + 'cache_locks', + 'chapters', + 'criteria', + 'failed_jobs', + 'glossaries', + 'images', + 'job_batches', + 'jobs', + 'migrations', + 'model_has_permissions', + 'model_has_roles', + 'password_reset_tokens', + 'permissions', + 'personal_access_tokens', + 'projects', + 'role_has_permissions', + 'roles', + 'sections', + 'sessions', + 'sub_criteria', + 'tables', + 'text_blocks', + 'time_blocks', + 'user_metas', + 'user_sub_criteria', + 'users', + 'years', +]; -Route::post('/register', [AuthController::class, 'register']); -Route::post('/login', [AuthController::class, 'login']); +// Auth +Route::post('auth/register', [AuthController::class, 'register']); +Route::post('auth/login', [AuthController::class, 'login']); + +Route::middleware('jwt') + ->prefix('auth') + ->group(function () { + Route::get('/', [AuthController::class, 'getUser']); + Route::put('/user', [AuthController::class, 'updateUser']); + Route::post('/logout', [AuthController::class, 'logout']); + }); + +// CRUD +foreach ($tables as $table) { + $controller = str_replace(' ', '', ucwords(str_replace('_', ' ', \Illuminate\Support\Str::singular($table)))) . 'Controller'; + + Route::middleware('jwt') + ->prefix("crud/{$table}") + ->group(function () use ($controller) { + Route::get('/', ["App\\Http\\Controllers\\$controller", 'index']); + Route::get('/{id}', ["App\\Http\\Controllers\\$controller", 'show']); + Route::post('/', ["App\\Http\\Controllers\\$controller", 'store']); + Route::put('/{id}', ["App\\Http\\Controllers\\$controller", 'update']); + Route::delete('/{id}', ["App\\Http\\Controllers\\$controller", 'delete']); + }); +} + +Route::middleware('jwt') + ->prefix('app') + ->group(function () { + Route::get('/projects/{project}', [AppController::class, 'getProject']); + }); -Route::middleware('jwt')->group(function () { - Route::get('/user', [AuthController::class, 'getUser']); - Route::put('/user', [AuthController::class, 'updateUser']); - Route::post('/logout', [AuthController::class, 'logout']); -}); diff --git a/apps/backend/routes/console.php b/apps/backend/routes/console.php deleted file mode 100644 index 3c9adf1..0000000 --- a/apps/backend/routes/console.php +++ /dev/null @@ -1,8 +0,0 @@ -comment(Inspiring::quote()); -})->purpose('Display an inspiring quote'); diff --git a/apps/backend/routes/web.php b/apps/backend/routes/web.php index 86a06c5..c2ecb84 100644 --- a/apps/backend/routes/web.php +++ b/apps/backend/routes/web.php @@ -3,5 +3,5 @@ use Illuminate\Support\Facades\Route; Route::get('/', function () { - return view('welcome'); + return json_encode(['status' => 'ok']); });